mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 17:47:31 +00:00
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
35 changed files with 1753 additions and 2068 deletions
|
@ -425,7 +425,7 @@ class ElectrumWindow(App):
|
||||||
Logger.debug('Electrum: Wallet not found. Launching install wizard')
|
Logger.debug('Electrum: Wallet not found. Launching install wizard')
|
||||||
wizard = Factory.InstallWizard(self.electrum_config, self.network, path)
|
wizard = Factory.InstallWizard(self.electrum_config, self.network, path)
|
||||||
wizard.bind(on_wizard_complete=self.on_wizard_complete)
|
wizard.bind(on_wizard_complete=self.on_wizard_complete)
|
||||||
action = wizard.get_action()
|
action = wizard.storage.get_action()
|
||||||
wizard.run(action)
|
wizard.run(action)
|
||||||
|
|
||||||
def on_stop(self):
|
def on_stop(self):
|
||||||
|
@ -562,7 +562,7 @@ class ElectrumWindow(App):
|
||||||
elif server_lag > 1:
|
elif server_lag > 1:
|
||||||
status = _("Server lagging (%d blocks)"%server_lag)
|
status = _("Server lagging (%d blocks)"%server_lag)
|
||||||
else:
|
else:
|
||||||
c, u, x = self.wallet.get_account_balance(self.current_account)
|
c, u, x = self.wallet.get_balance(self.current_account)
|
||||||
text = self.format_amount(c+x+u)
|
text = self.format_amount(c+x+u)
|
||||||
status = str(text.strip() + ' ' + self.base_unit)
|
status = str(text.strip() + ' ' + self.base_unit)
|
||||||
else:
|
else:
|
||||||
|
@ -749,7 +749,7 @@ class ElectrumWindow(App):
|
||||||
popup.open()
|
popup.open()
|
||||||
|
|
||||||
def protected(self, msg, f, args):
|
def protected(self, msg, f, args):
|
||||||
if self.wallet.use_encryption:
|
if self.wallet.has_password():
|
||||||
self.password_dialog(msg, f, args)
|
self.password_dialog(msg, f, args)
|
||||||
else:
|
else:
|
||||||
apply(f, args + (None,))
|
apply(f, args + (None,))
|
||||||
|
@ -769,7 +769,7 @@ class ElectrumWindow(App):
|
||||||
wallet_path = self.get_wallet_path()
|
wallet_path = self.get_wallet_path()
|
||||||
dirname = os.path.dirname(wallet_path)
|
dirname = os.path.dirname(wallet_path)
|
||||||
basename = os.path.basename(wallet_path)
|
basename = os.path.basename(wallet_path)
|
||||||
if self.wallet.use_encryption:
|
if self.wallet.has_password():
|
||||||
try:
|
try:
|
||||||
self.wallet.check_password(pw)
|
self.wallet.check_password(pw)
|
||||||
except:
|
except:
|
||||||
|
@ -787,7 +787,7 @@ class ElectrumWindow(App):
|
||||||
self.protected(_("Enter your PIN code in order to decrypt your seed"), self._show_seed, (label,))
|
self.protected(_("Enter your PIN code in order to decrypt your seed"), self._show_seed, (label,))
|
||||||
|
|
||||||
def _show_seed(self, label, password):
|
def _show_seed(self, label, password):
|
||||||
if self.wallet.use_encryption and password is None:
|
if self.wallet.has_password() and password is None:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
seed = self.wallet.get_seed(password)
|
seed = self.wallet.get_seed(password)
|
||||||
|
@ -797,13 +797,13 @@ class ElectrumWindow(App):
|
||||||
label.text = _('Seed') + ':\n' + seed
|
label.text = _('Seed') + ':\n' + seed
|
||||||
|
|
||||||
def change_password(self, cb):
|
def change_password(self, cb):
|
||||||
if self.wallet.use_encryption:
|
if self.wallet.has_password():
|
||||||
self.protected(_("Changing PIN code.") + '\n' + _("Enter your current PIN:"), self._change_password, (cb,))
|
self.protected(_("Changing PIN code.") + '\n' + _("Enter your current PIN:"), self._change_password, (cb,))
|
||||||
else:
|
else:
|
||||||
self._change_password(cb, None)
|
self._change_password(cb, None)
|
||||||
|
|
||||||
def _change_password(self, cb, old_password):
|
def _change_password(self, cb, old_password):
|
||||||
if self.wallet.use_encryption:
|
if self.wallet.has_password():
|
||||||
if old_password is None:
|
if old_password is None:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -742,7 +742,7 @@ class InstallWizard(BaseWizard, Widget):
|
||||||
def request_password(self, run_next):
|
def request_password(self, run_next):
|
||||||
def callback(pin):
|
def callback(pin):
|
||||||
if pin:
|
if pin:
|
||||||
self.run('confirm_password', (pin, run_next))
|
self.run('confirm_password', pin, run_next)
|
||||||
else:
|
else:
|
||||||
run_next(None)
|
run_next(None)
|
||||||
self.password_dialog('Choose a PIN code', callback)
|
self.password_dialog('Choose a PIN code', callback)
|
||||||
|
@ -753,7 +753,7 @@ class InstallWizard(BaseWizard, Widget):
|
||||||
run_next(pin)
|
run_next(pin)
|
||||||
else:
|
else:
|
||||||
self.show_error(_('PIN mismatch'))
|
self.show_error(_('PIN mismatch'))
|
||||||
self.run('request_password', (run_next,))
|
self.run('request_password', run_next)
|
||||||
self.password_dialog('Confirm your PIN code', callback)
|
self.password_dialog('Confirm your PIN code', callback)
|
||||||
|
|
||||||
def action_dialog(self, action, run_next):
|
def action_dialog(self, action, run_next):
|
||||||
|
|
|
@ -331,7 +331,7 @@ class ReceiveScreen(CScreen):
|
||||||
def get_new_address(self):
|
def get_new_address(self):
|
||||||
if not self.app.wallet:
|
if not self.app.wallet:
|
||||||
return False
|
return False
|
||||||
addr = self.app.wallet.get_unused_address(None)
|
addr = self.app.wallet.get_unused_address()
|
||||||
if addr is None:
|
if addr is None:
|
||||||
return False
|
return False
|
||||||
self.clear()
|
self.clear()
|
||||||
|
|
|
@ -163,8 +163,8 @@ class ElectrumGui:
|
||||||
wallet = wizard.run_and_get_wallet()
|
wallet = wizard.run_and_get_wallet()
|
||||||
if not wallet:
|
if not wallet:
|
||||||
return
|
return
|
||||||
if wallet.get_action():
|
#if wallet.get_action():
|
||||||
return
|
# return
|
||||||
self.daemon.add_wallet(wallet)
|
self.daemon.add_wallet(wallet)
|
||||||
w = self.create_window_for_wallet(wallet)
|
w = self.create_window_for_wallet(wallet)
|
||||||
if uri:
|
if uri:
|
||||||
|
|
|
@ -41,26 +41,14 @@ class AddressList(MyTreeWidget):
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
self.wallet = self.parent.wallet
|
self.wallet = self.parent.wallet
|
||||||
self.accounts_expanded = self.wallet.storage.get('accounts_expanded', {})
|
|
||||||
item = self.currentItem()
|
item = self.currentItem()
|
||||||
current_address = item.data(0, Qt.UserRole).toString() if item else None
|
current_address = item.data(0, Qt.UserRole).toString() if item else None
|
||||||
self.clear()
|
self.clear()
|
||||||
accounts = self.wallet.get_accounts()
|
receiving_addresses = self.wallet.get_receiving_addresses()
|
||||||
if self.parent.current_account is None:
|
change_addresses = self.wallet.get_change_addresses()
|
||||||
account_items = sorted(accounts.items())
|
if True:
|
||||||
else:
|
|
||||||
account_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
|
account_item = self
|
||||||
sequences = [0,1] if account.has_change() else [0]
|
sequences = [0,1] if change_addresses else [0]
|
||||||
for is_change in sequences:
|
for is_change in sequences:
|
||||||
if len(sequences) > 1:
|
if len(sequences) > 1:
|
||||||
name = _("Receiving") if not is_change else _("Change")
|
name = _("Receiving") if not is_change else _("Change")
|
||||||
|
@ -72,7 +60,7 @@ class AddressList(MyTreeWidget):
|
||||||
seq_item = account_item
|
seq_item = account_item
|
||||||
used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
|
used_item = QTreeWidgetItem( [ _("Used"), '', '', '', ''] )
|
||||||
used_flag = False
|
used_flag = False
|
||||||
addr_list = account.get_addresses(is_change)
|
addr_list = change_addresses if is_change else receiving_addresses
|
||||||
for address in addr_list:
|
for address in addr_list:
|
||||||
num = len(self.wallet.history.get(address,[]))
|
num = len(self.wallet.history.get(address,[]))
|
||||||
is_used = self.wallet.is_used(address)
|
is_used = self.wallet.is_used(address)
|
||||||
|
@ -85,7 +73,7 @@ class AddressList(MyTreeWidget):
|
||||||
address_item.setData(0, Qt.UserRole+1, True) # label can be edited
|
address_item.setData(0, Qt.UserRole+1, True) # label can be edited
|
||||||
if self.wallet.is_frozen(address):
|
if self.wallet.is_frozen(address):
|
||||||
address_item.setBackgroundColor(0, QColor('lightblue'))
|
address_item.setBackgroundColor(0, QColor('lightblue'))
|
||||||
if self.wallet.is_beyond_limit(address, account, is_change):
|
if self.wallet.is_beyond_limit(address, is_change):
|
||||||
address_item.setBackgroundColor(0, QColor('red'))
|
address_item.setBackgroundColor(0, QColor('red'))
|
||||||
if is_used:
|
if is_used:
|
||||||
if not used_flag:
|
if not used_flag:
|
||||||
|
@ -107,8 +95,9 @@ class AddressList(MyTreeWidget):
|
||||||
address_item.addChild(utxo_item)
|
address_item.addChild(utxo_item)
|
||||||
|
|
||||||
def create_menu(self, position):
|
def create_menu(self, position):
|
||||||
from electrum.wallet import Multisig_Wallet
|
from electrum.wallet import Multisig_Wallet, Imported_Wallet
|
||||||
is_multisig = isinstance(self.wallet, Multisig_Wallet)
|
is_multisig = isinstance(self.wallet, Multisig_Wallet)
|
||||||
|
is_imported = isinstance(self.wallet, Imported_Wallet)
|
||||||
selected = self.selectedItems()
|
selected = self.selectedItems()
|
||||||
multi_select = len(selected) > 1
|
multi_select = len(selected) > 1
|
||||||
addrs = [unicode(item.text(0)) for item in selected]
|
addrs = [unicode(item.text(0)) for item in selected]
|
||||||
|
@ -142,7 +131,7 @@ class AddressList(MyTreeWidget):
|
||||||
if not is_multisig and not self.wallet.is_watching_only():
|
if not is_multisig and not self.wallet.is_watching_only():
|
||||||
menu.addAction(_("Sign/verify message"), lambda: self.parent.sign_verify_message(addr))
|
menu.addAction(_("Sign/verify message"), lambda: self.parent.sign_verify_message(addr))
|
||||||
menu.addAction(_("Encrypt/decrypt message"), lambda: self.parent.encrypt_message(addr))
|
menu.addAction(_("Encrypt/decrypt message"), lambda: self.parent.encrypt_message(addr))
|
||||||
if self.wallet.is_imported(addr):
|
if is_imported:
|
||||||
menu.addAction(_("Remove from wallet"), lambda: self.parent.delete_imported_key(addr))
|
menu.addAction(_("Remove from wallet"), lambda: self.parent.delete_imported_key(addr))
|
||||||
addr_URL = block_explorer_URL(self.config, 'addr', addr)
|
addr_URL = block_explorer_URL(self.config, 'addr', addr)
|
||||||
if addr_URL:
|
if addr_URL:
|
||||||
|
@ -161,18 +150,3 @@ class AddressList(MyTreeWidget):
|
||||||
run_hook('receive_menu', menu, addrs, self.wallet)
|
run_hook('receive_menu', menu, addrs, self.wallet)
|
||||||
menu.exec_(self.viewport().mapToGlobal(position))
|
menu.exec_(self.viewport().mapToGlobal(position))
|
||||||
|
|
||||||
def create_account_menu(self, position, k, item):
|
|
||||||
menu = QMenu()
|
|
||||||
exp = item.isExpanded()
|
|
||||||
menu.addAction(_("Minimize") if exp else _("Maximize"), lambda: self.set_account_expanded(item, k, not exp))
|
|
||||||
menu.addAction(_("Rename"), lambda: self.parent.edit_account_label(k))
|
|
||||||
if self.wallet.seed_version > 4:
|
|
||||||
menu.addAction(_("View details"), lambda: self.parent.show_account_details(k))
|
|
||||||
menu.exec_(self.viewport().mapToGlobal(position))
|
|
||||||
|
|
||||||
def set_account_expanded(self, item, k, b):
|
|
||||||
item.setExpanded(b)
|
|
||||||
self.accounts_expanded[k] = b
|
|
||||||
|
|
||||||
def on_close(self):
|
|
||||||
self.wallet.storage.put('accounts_expanded', self.accounts_expanded)
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ class HistoryList(MyTreeWidget):
|
||||||
|
|
||||||
def get_domain(self):
|
def get_domain(self):
|
||||||
'''Replaced in address_dialog.py'''
|
'''Replaced in address_dialog.py'''
|
||||||
return self.wallet.get_account_addresses(self.parent.current_account)
|
return self.wallet.get_addresses()
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
self.wallet = self.parent.wallet
|
self.wallet = self.parent.wallet
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
from PyQt4.QtGui import *
|
from PyQt4.QtGui import *
|
||||||
from PyQt4.QtCore import *
|
from PyQt4.QtCore import *
|
||||||
|
@ -156,23 +157,48 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
||||||
if self.config.get('auto_connect') is None:
|
if self.config.get('auto_connect') is None:
|
||||||
self.choose_server(self.network)
|
self.choose_server(self.network)
|
||||||
|
|
||||||
action = self.get_action()
|
|
||||||
if action != 'new':
|
|
||||||
self.hide()
|
|
||||||
path = self.storage.path
|
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()
|
||||||
msg = _("The file '%s' contains an incompletely created wallet.\n"
|
msg = _("The file '%s' contains an incompletely created wallet.\n"
|
||||||
"Do you want to complete its creation now?") % path
|
"Do you want to complete its creation now?") % path
|
||||||
if not self.question(msg):
|
if not self.question(msg):
|
||||||
if self.question(_("Do you want to delete '%s'?") % path):
|
if self.question(_("Do you want to delete '%s'?") % path):
|
||||||
import os
|
|
||||||
os.remove(path)
|
os.remove(path)
|
||||||
self.show_warning(_('The file was removed'))
|
self.show_warning(_('The file was removed'))
|
||||||
return
|
return
|
||||||
return
|
|
||||||
self.show()
|
self.show()
|
||||||
|
if action:
|
||||||
|
# self.wallet is set in run
|
||||||
self.run(action)
|
self.run(action)
|
||||||
return self.wallet
|
return self.wallet
|
||||||
|
|
||||||
|
|
||||||
def finished(self):
|
def finished(self):
|
||||||
'''Ensure the dialog is closed.'''
|
'''Ensure the dialog is closed.'''
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
|
@ -51,7 +51,7 @@ from electrum.util import (block_explorer, block_explorer_info, format_time,
|
||||||
from electrum import Transaction, mnemonic
|
from electrum import Transaction, mnemonic
|
||||||
from electrum import util, bitcoin, commands, coinchooser
|
from electrum import util, bitcoin, commands, coinchooser
|
||||||
from electrum import SimpleConfig, paymentrequest
|
from electrum import SimpleConfig, paymentrequest
|
||||||
from electrum.wallet import Wallet, BIP32_RD_Wallet, Multisig_Wallet
|
from electrum.wallet import Wallet, Multisig_Wallet
|
||||||
|
|
||||||
from amountedit import BTCAmountEdit, MyLineEdit, BTCkBEdit
|
from amountedit import BTCAmountEdit, MyLineEdit, BTCkBEdit
|
||||||
from network_dialog import NetworkDialog
|
from network_dialog import NetworkDialog
|
||||||
|
@ -248,21 +248,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
t.setDaemon(True)
|
t.setDaemon(True)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
def update_account_selector(self):
|
|
||||||
# account selector
|
|
||||||
accounts = self.wallet.get_account_names()
|
|
||||||
self.account_selector.clear()
|
|
||||||
if len(accounts) > 1:
|
|
||||||
self.account_selector.addItems([_("All accounts")] + accounts.values())
|
|
||||||
self.account_selector.setCurrentIndex(0)
|
|
||||||
self.account_selector.show()
|
|
||||||
else:
|
|
||||||
self.account_selector.hide()
|
|
||||||
|
|
||||||
def close_wallet(self):
|
def close_wallet(self):
|
||||||
if self.wallet:
|
if self.wallet:
|
||||||
self.print_error('close_wallet', self.wallet.storage.path)
|
self.print_error('close_wallet', self.wallet.storage.path)
|
||||||
self.address_list.on_close()
|
|
||||||
run_hook('close_wallet', self.wallet)
|
run_hook('close_wallet', self.wallet)
|
||||||
|
|
||||||
def load_wallet(self, wallet):
|
def load_wallet(self, wallet):
|
||||||
|
@ -270,13 +258,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.wallet = wallet
|
self.wallet = wallet
|
||||||
self.update_recently_visited(wallet.storage.path)
|
self.update_recently_visited(wallet.storage.path)
|
||||||
# address used to create a dummy transaction and estimate transaction fee
|
# address used to create a dummy transaction and estimate transaction fee
|
||||||
self.current_account = self.wallet.storage.get("current_account", None)
|
|
||||||
self.history_list.update()
|
self.history_list.update()
|
||||||
self.need_update.set()
|
self.need_update.set()
|
||||||
# Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
|
# Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
|
||||||
self.notify_transactions()
|
self.notify_transactions()
|
||||||
# update menus
|
# update menus
|
||||||
self.update_new_account_menu()
|
|
||||||
self.seed_menu.setEnabled(self.wallet.has_seed())
|
self.seed_menu.setEnabled(self.wallet.has_seed())
|
||||||
self.mpk_menu.setEnabled(self.wallet.is_deterministic())
|
self.mpk_menu.setEnabled(self.wallet.is_deterministic())
|
||||||
self.update_lock_icon()
|
self.update_lock_icon()
|
||||||
|
@ -391,8 +377,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
|
|
||||||
wallet_menu = menubar.addMenu(_("&Wallet"))
|
wallet_menu = menubar.addMenu(_("&Wallet"))
|
||||||
wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
|
wallet_menu.addAction(_("&New contact"), self.new_contact_dialog)
|
||||||
self.new_account_menu = wallet_menu.addAction(_("&New account"), self.new_account_dialog)
|
|
||||||
|
|
||||||
wallet_menu.addSeparator()
|
wallet_menu.addSeparator()
|
||||||
|
|
||||||
self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
|
self.password_menu = wallet_menu.addAction(_("&Password"), self.change_password_dialog)
|
||||||
|
@ -569,7 +553,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
text = _("Server is lagging (%d blocks)"%server_lag)
|
text = _("Server is lagging (%d blocks)"%server_lag)
|
||||||
icon = QIcon(":icons/status_lagging.png")
|
icon = QIcon(":icons/status_lagging.png")
|
||||||
else:
|
else:
|
||||||
c, u, x = self.wallet.get_account_balance(self.current_account)
|
c, u, x = self.wallet.get_balance()
|
||||||
text = _("Balance" ) + ": %s "%(self.format_amount_and_units(c))
|
text = _("Balance" ) + ": %s "%(self.format_amount_and_units(c))
|
||||||
if u:
|
if u:
|
||||||
text += " [%s unconfirmed]"%(self.format_amount(u, True).strip())
|
text += " [%s unconfirmed]"%(self.format_amount(u, True).strip())
|
||||||
|
@ -593,8 +577,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.update_status()
|
self.update_status()
|
||||||
if self.wallet.up_to_date or not self.network or not self.network.is_connected():
|
if self.wallet.up_to_date or not self.network or not self.network.is_connected():
|
||||||
self.update_tabs()
|
self.update_tabs()
|
||||||
if self.wallet.up_to_date:
|
|
||||||
self.check_next_account()
|
|
||||||
|
|
||||||
def update_tabs(self):
|
def update_tabs(self):
|
||||||
self.history_list.update()
|
self.history_list.update()
|
||||||
|
@ -788,7 +770,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.saved = True
|
self.saved = True
|
||||||
|
|
||||||
def new_payment_request(self):
|
def new_payment_request(self):
|
||||||
addr = self.wallet.get_unused_address(self.current_account)
|
addr = self.wallet.get_unused_address(None)
|
||||||
if addr is None:
|
if addr is None:
|
||||||
from electrum.wallet import Imported_Wallet
|
from electrum.wallet import Imported_Wallet
|
||||||
if isinstance(self.wallet, Imported_Wallet):
|
if isinstance(self.wallet, Imported_Wallet):
|
||||||
|
@ -796,7 +778,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
return
|
return
|
||||||
if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")):
|
if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")):
|
||||||
return
|
return
|
||||||
addr = self.wallet.create_new_address(self.current_account, False)
|
addr = self.wallet.create_new_address(None, False)
|
||||||
self.set_receive_address(addr)
|
self.set_receive_address(addr)
|
||||||
self.expires_label.hide()
|
self.expires_label.hide()
|
||||||
self.expires_combo.show()
|
self.expires_combo.show()
|
||||||
|
@ -809,7 +791,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.receive_amount_e.setAmount(None)
|
self.receive_amount_e.setAmount(None)
|
||||||
|
|
||||||
def clear_receive_tab(self):
|
def clear_receive_tab(self):
|
||||||
addr = self.wallet.get_unused_address(self.current_account)
|
addr = self.wallet.get_unused_address()
|
||||||
self.receive_address_e.setText(addr if addr else '')
|
self.receive_address_e.setText(addr if addr else '')
|
||||||
self.receive_message_e.setText('')
|
self.receive_message_e.setText('')
|
||||||
self.receive_amount_e.setAmount(None)
|
self.receive_amount_e.setAmount(None)
|
||||||
|
@ -1102,7 +1084,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
def request_password(self, *args, **kwargs):
|
def request_password(self, *args, **kwargs):
|
||||||
parent = self.top_level_window()
|
parent = self.top_level_window()
|
||||||
password = None
|
password = None
|
||||||
while self.wallet.use_encryption:
|
while self.wallet.has_password():
|
||||||
password = self.password_dialog(parent=parent)
|
password = self.password_dialog(parent=parent)
|
||||||
try:
|
try:
|
||||||
if password:
|
if password:
|
||||||
|
@ -1208,7 +1190,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
if tx.get_fee() >= self.config.get('confirm_fee', 100000):
|
if tx.get_fee() >= self.config.get('confirm_fee', 100000):
|
||||||
msg.append(_('Warning')+ ': ' + _("The fee for this transaction seems unusually high."))
|
msg.append(_('Warning')+ ': ' + _("The fee for this transaction seems unusually high."))
|
||||||
|
|
||||||
if self.wallet.use_encryption:
|
if self.wallet.has_password():
|
||||||
msg.append("")
|
msg.append("")
|
||||||
msg.append(_("Enter your password to proceed"))
|
msg.append(_("Enter your password to proceed"))
|
||||||
password = self.password_dialog('\n'.join(msg))
|
password = self.password_dialog('\n'.join(msg))
|
||||||
|
@ -1237,7 +1219,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
'''Sign the transaction in a separate thread. When done, calls
|
'''Sign the transaction in a separate thread. When done, calls
|
||||||
the callback with a success code of True or False.
|
the callback with a success code of True or False.
|
||||||
'''
|
'''
|
||||||
if self.wallet.use_encryption and not password:
|
if self.wallet.has_password() and not password:
|
||||||
callback(False) # User cancelled password input
|
callback(False) # User cancelled password input
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -1438,7 +1420,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
if self.pay_from:
|
if self.pay_from:
|
||||||
return self.pay_from
|
return self.pay_from
|
||||||
else:
|
else:
|
||||||
domain = self.wallet.get_account_addresses(self.current_account)
|
domain = self.wallet.get_addresses()
|
||||||
return self.wallet.get_spendable_coins(domain)
|
return self.wallet.get_spendable_coins(domain)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1561,18 +1543,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
console.updateNamespace(methods)
|
console.updateNamespace(methods)
|
||||||
|
|
||||||
|
|
||||||
def change_account(self,s):
|
|
||||||
if s == _("All accounts"):
|
|
||||||
self.current_account = None
|
|
||||||
else:
|
|
||||||
accounts = self.wallet.get_account_names()
|
|
||||||
for k, v in accounts.items():
|
|
||||||
if v == s:
|
|
||||||
self.current_account = k
|
|
||||||
self.history_list.update()
|
|
||||||
self.update_status()
|
|
||||||
self.address_list.update()
|
|
||||||
self.request_list.update()
|
|
||||||
|
|
||||||
def create_status_bar(self):
|
def create_status_bar(self):
|
||||||
|
|
||||||
|
@ -1583,11 +1553,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.balance_label = QLabel("")
|
self.balance_label = QLabel("")
|
||||||
sb.addWidget(self.balance_label)
|
sb.addWidget(self.balance_label)
|
||||||
|
|
||||||
self.account_selector = QComboBox()
|
|
||||||
self.account_selector.setSizeAdjustPolicy(QComboBox.AdjustToContents)
|
|
||||||
self.connect(self.account_selector, SIGNAL("activated(QString)"), self.change_account)
|
|
||||||
sb.addPermanentWidget(self.account_selector)
|
|
||||||
|
|
||||||
self.search_box = QLineEdit()
|
self.search_box = QLineEdit()
|
||||||
self.search_box.textChanged.connect(self.do_search)
|
self.search_box.textChanged.connect(self.do_search)
|
||||||
self.search_box.hide()
|
self.search_box.hide()
|
||||||
|
@ -1606,7 +1571,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.setStatusBar(sb)
|
self.setStatusBar(sb)
|
||||||
|
|
||||||
def update_lock_icon(self):
|
def update_lock_icon(self):
|
||||||
icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
|
icon = QIcon(":icons/lock.png") if self.wallet.has_password() else QIcon(":icons/unlock.png")
|
||||||
self.password_button.setIcon(icon)
|
self.password_button.setIcon(icon)
|
||||||
|
|
||||||
def update_buttons_on_seed(self):
|
def update_buttons_on_seed(self):
|
||||||
|
@ -1619,7 +1584,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
|
|
||||||
msg = (_('Your wallet is encrypted. Use this dialog to change your '
|
msg = (_('Your wallet is encrypted. Use this dialog to change your '
|
||||||
'password. To disable wallet encryption, enter an empty new '
|
'password. To disable wallet encryption, enter an empty new '
|
||||||
'password.') if self.wallet.use_encryption
|
'password.') if self.wallet.has_password()
|
||||||
else _('Your wallet keys are not encrypted'))
|
else _('Your wallet keys are not encrypted'))
|
||||||
d = PasswordDialog(self, self.wallet, msg, PW_CHANGE)
|
d = PasswordDialog(self, self.wallet, msg, PW_CHANGE)
|
||||||
ok, password, new_password = d.run()
|
ok, password, new_password = d.run()
|
||||||
|
@ -1684,48 +1649,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
if self.set_contact(unicode(line2.text()), str(line1.text())):
|
if self.set_contact(unicode(line2.text()), str(line1.text())):
|
||||||
self.tabs.setCurrentIndex(4)
|
self.tabs.setCurrentIndex(4)
|
||||||
|
|
||||||
def update_new_account_menu(self):
|
|
||||||
self.new_account_menu.setVisible(self.wallet.can_create_accounts())
|
|
||||||
self.new_account_menu.setEnabled(self.wallet.permit_account_naming())
|
|
||||||
self.update_account_selector()
|
|
||||||
|
|
||||||
def new_account_dialog(self):
|
|
||||||
dialog = WindowModalDialog(self, _("New Account Name"))
|
|
||||||
vbox = QVBoxLayout()
|
|
||||||
msg = _("Enter a name to give the account. You will not be "
|
|
||||||
"permitted to create further accounts until the new account "
|
|
||||||
"receives at least one transaction.") + "\n"
|
|
||||||
label = QLabel(msg)
|
|
||||||
label.setWordWrap(True)
|
|
||||||
vbox.addWidget(label)
|
|
||||||
e = QLineEdit()
|
|
||||||
vbox.addWidget(e)
|
|
||||||
vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog)))
|
|
||||||
dialog.setLayout(vbox)
|
|
||||||
if dialog.exec_():
|
|
||||||
self.wallet.set_label(self.wallet.last_account_id(), str(e.text()))
|
|
||||||
self.address_list.update()
|
|
||||||
self.tabs.setCurrentIndex(3)
|
|
||||||
self.update_new_account_menu()
|
|
||||||
|
|
||||||
def check_next_account(self):
|
|
||||||
if self.wallet.needs_next_account() and not self.checking_accounts:
|
|
||||||
self.checking_accounts = True
|
|
||||||
msg = _("All the accounts in your wallet have received "
|
|
||||||
"transactions. Electrum must check whether more "
|
|
||||||
"accounts exist; one will only be shown if "
|
|
||||||
"it has been used or you give it a name.")
|
|
||||||
self.show_message(msg, title=_("Check Accounts"))
|
|
||||||
self.create_next_account()
|
|
||||||
|
|
||||||
@protected
|
|
||||||
def create_next_account(self, password):
|
|
||||||
def on_done():
|
|
||||||
self.checking_accounts = False
|
|
||||||
self.update_new_account_menu()
|
|
||||||
task = partial(self.wallet.create_next_account, password)
|
|
||||||
self.wallet.thread.add(task, on_done=on_done)
|
|
||||||
|
|
||||||
def show_master_public_keys(self):
|
def show_master_public_keys(self):
|
||||||
dialog = WindowModalDialog(self, "Master Public Keys")
|
dialog = WindowModalDialog(self, "Master Public Keys")
|
||||||
mpk_dict = self.wallet.get_master_public_keys()
|
mpk_dict = self.wallet.get_master_public_keys()
|
||||||
|
@ -1741,7 +1664,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
if len(mpk_dict) > 1:
|
if len(mpk_dict) > 1:
|
||||||
def label(key):
|
def label(key):
|
||||||
if isinstance(self.wallet, Multisig_Wallet):
|
if isinstance(self.wallet, Multisig_Wallet):
|
||||||
is_mine = self.wallet.master_private_keys.has_key(key)
|
is_mine = False#self.wallet.master_private_keys.has_key(key)
|
||||||
mine_text = [_("cosigner"), _("self")]
|
mine_text = [_("cosigner"), _("self")]
|
||||||
return "%s (%s)" % (key, mine_text[is_mine])
|
return "%s (%s)" % (key, mine_text[is_mine])
|
||||||
return key
|
return key
|
||||||
|
@ -1759,19 +1682,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
def show_seed_dialog(self, password):
|
def show_seed_dialog(self, password):
|
||||||
if self.wallet.use_encryption and password is None:
|
if self.wallet.has_password() and password is None:
|
||||||
return # User cancelled password input
|
# User cancelled password input
|
||||||
|
return
|
||||||
if not self.wallet.has_seed():
|
if not self.wallet.has_seed():
|
||||||
self.show_message(_('This wallet has no seed'))
|
self.show_message(_('This wallet has no seed'))
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mnemonic = self.wallet.get_mnemonic(password)
|
mnemonic = self.wallet.get_mnemonic(password)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
self.show_error(str(e))
|
self.show_error(str(e))
|
||||||
return
|
return
|
||||||
from seed_dialog import SeedDialog
|
from seed_dialog import SeedDialog
|
||||||
d = SeedDialog(self, mnemonic, self.wallet.has_imported_keys())
|
d = SeedDialog(self, mnemonic)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
|
||||||
|
|
||||||
|
@ -1795,9 +1718,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
d.setMinimumSize(600, 200)
|
d.setMinimumSize(600, 200)
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
vbox.addWidget( QLabel(_("Address") + ': ' + address))
|
vbox.addWidget( QLabel(_("Address") + ': ' + address))
|
||||||
if isinstance(self.wallet, BIP32_RD_Wallet):
|
#if isinstance(self.wallet, BIP32_RD_Wallet):
|
||||||
derivation = self.wallet.address_id(address)
|
# derivation = self.wallet.address_id(address)
|
||||||
vbox.addWidget(QLabel(_("Derivation") + ': ' + derivation))
|
# vbox.addWidget(QLabel(_("Derivation") + ': ' + derivation))
|
||||||
vbox.addWidget(QLabel(_("Public key") + ':'))
|
vbox.addWidget(QLabel(_("Public key") + ':'))
|
||||||
keys_e = ShowQRTextEdit(text='\n'.join(pubkey_list))
|
keys_e = ShowQRTextEdit(text='\n'.join(pubkey_list))
|
||||||
keys_e.addCopyButton(self.app)
|
keys_e.addCopyButton(self.app)
|
||||||
|
@ -2045,7 +1968,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
if self.wallet.is_watching_only():
|
if self.wallet.is_watching_only():
|
||||||
self.show_message(_("This is a watching-only wallet"))
|
self.show_message(_("This is a watching-only wallet"))
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.wallet.check_password(password)
|
self.wallet.check_password(password)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -2235,7 +2157,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
keys_e.setTabChangesFocus(True)
|
keys_e.setTabChangesFocus(True)
|
||||||
vbox.addWidget(keys_e)
|
vbox.addWidget(keys_e)
|
||||||
|
|
||||||
addresses = self.wallet.get_unused_addresses(self.current_account)
|
addresses = self.wallet.get_unused_addresses(None)
|
||||||
h, address_e = address_field(addresses)
|
h, address_e = address_field(addresses)
|
||||||
vbox.addLayout(h)
|
vbox.addLayout(h)
|
||||||
|
|
||||||
|
@ -2271,19 +2193,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
def do_import_privkey(self, password):
|
def do_import_privkey(self, password):
|
||||||
if not self.wallet.has_imported_keys():
|
if not self.wallet.keystore.can_import():
|
||||||
if not self.question('<b>'+_('Warning') +':\n</b><br/>'+ _('Imported keys are not recoverable from seed.') + ' ' \
|
|
||||||
+ _('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
|
return
|
||||||
|
|
||||||
text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
|
text = text_dialog(self, _('Import private keys'), _("Enter private keys")+':', _("Import"))
|
||||||
if not text: return
|
if not text:
|
||||||
|
return
|
||||||
text = str(text).split()
|
text = str(text).split()
|
||||||
badkeys = []
|
badkeys = []
|
||||||
addrlist = []
|
addrlist = []
|
||||||
for key in text:
|
for key in text:
|
||||||
|
addr = self.wallet.import_key(key, password)
|
||||||
try:
|
try:
|
||||||
addr = self.wallet.import_key(key, password)
|
addr = self.wallet.import_key(key, password)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -2673,25 +2592,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
vbox.addLayout(Buttons(CloseButton(d)))
|
vbox.addLayout(Buttons(CloseButton(d)))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
|
||||||
def show_account_details(self, k):
|
|
||||||
account = self.wallet.accounts[k]
|
|
||||||
d = WindowModalDialog(self, _('Account Details'))
|
|
||||||
vbox = QVBoxLayout(d)
|
|
||||||
name = self.wallet.get_account_name(k)
|
|
||||||
label = QLabel('Name: ' + name)
|
|
||||||
vbox.addWidget(label)
|
|
||||||
vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type()))
|
|
||||||
vbox.addWidget(QLabel(_('Derivation') + ': ' + k))
|
|
||||||
vbox.addWidget(QLabel(_('Master Public Key:')))
|
|
||||||
text = QTextEdit()
|
|
||||||
text.setReadOnly(True)
|
|
||||||
text.setMaximumHeight(170)
|
|
||||||
vbox.addWidget(text)
|
|
||||||
mpk_text = '\n'.join(account.get_master_pubkeys())
|
|
||||||
text.setText(mpk_text)
|
|
||||||
vbox.addLayout(Buttons(CloseButton(d)))
|
|
||||||
d.exec_()
|
|
||||||
|
|
||||||
def bump_fee_dialog(self, tx):
|
def bump_fee_dialog(self, tx):
|
||||||
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
|
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
|
||||||
d = WindowModalDialog(self, _('Bump Fee'))
|
d = WindowModalDialog(self, _('Bump Fee'))
|
||||||
|
|
|
@ -94,7 +94,7 @@ class PasswordLayout(object):
|
||||||
|
|
||||||
m1 = _('New Password:') if kind == PW_NEW else _('Password:')
|
m1 = _('New Password:') if kind == PW_NEW else _('Password:')
|
||||||
msgs = [m1, _('Confirm Password:')]
|
msgs = [m1, _('Confirm Password:')]
|
||||||
if wallet and wallet.use_encryption:
|
if wallet and wallet.has_password():
|
||||||
grid.addWidget(QLabel(_('Current Password:')), 0, 0)
|
grid.addWidget(QLabel(_('Current Password:')), 0, 0)
|
||||||
grid.addWidget(self.pw, 0, 1)
|
grid.addWidget(self.pw, 0, 1)
|
||||||
lockfile = ":icons/lock.png"
|
lockfile = ":icons/lock.png"
|
||||||
|
|
|
@ -36,20 +36,19 @@ from util import MyTreeWidget, pr_tooltips, pr_icons
|
||||||
class RequestList(MyTreeWidget):
|
class RequestList(MyTreeWidget):
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
MyTreeWidget.__init__(self, parent, self.create_menu, [_('Date'), _('Account'), _('Address'), '', _('Description'), _('Amount'), _('Status')], 4)
|
MyTreeWidget.__init__(self, parent, self.create_menu, [_('Date'), _('Address'), '', _('Description'), _('Amount'), _('Status')], 3)
|
||||||
self.currentItemChanged.connect(self.item_changed)
|
self.currentItemChanged.connect(self.item_changed)
|
||||||
self.itemClicked.connect(self.item_changed)
|
self.itemClicked.connect(self.item_changed)
|
||||||
self.setSortingEnabled(True)
|
self.setSortingEnabled(True)
|
||||||
self.setColumnWidth(0, 180)
|
self.setColumnWidth(0, 180)
|
||||||
self.hideColumn(1)
|
self.hideColumn(1)
|
||||||
self.hideColumn(2)
|
|
||||||
|
|
||||||
def item_changed(self, item):
|
def item_changed(self, item):
|
||||||
if item is None:
|
if item is None:
|
||||||
return
|
return
|
||||||
if not self.isItemSelected(item):
|
if not self.isItemSelected(item):
|
||||||
return
|
return
|
||||||
addr = str(item.text(2))
|
addr = str(item.text(1))
|
||||||
req = self.wallet.receive_requests[addr]
|
req = self.wallet.receive_requests[addr]
|
||||||
expires = age(req['time'] + req['exp']) if req.get('exp') else _('Never')
|
expires = age(req['time'] + req['exp']) if req.get('exp') else _('Never')
|
||||||
amount = req['amount']
|
amount = req['amount']
|
||||||
|
@ -72,13 +71,10 @@ class RequestList(MyTreeWidget):
|
||||||
self.parent.expires_label.hide()
|
self.parent.expires_label.hide()
|
||||||
self.parent.expires_combo.show()
|
self.parent.expires_combo.show()
|
||||||
|
|
||||||
# check if it is necessary to show the account
|
|
||||||
self.setColumnHidden(1, len(self.wallet.get_accounts()) == 1)
|
|
||||||
|
|
||||||
# update the receive address if necessary
|
# update the receive address if necessary
|
||||||
current_address = self.parent.receive_address_e.text()
|
current_address = self.parent.receive_address_e.text()
|
||||||
domain = self.wallet.get_account_addresses(self.parent.current_account, include_change=False)
|
domain = self.wallet.get_receiving_addresses()
|
||||||
addr = self.wallet.get_unused_address(self.parent.current_account)
|
addr = self.wallet.get_unused_address()
|
||||||
if not current_address in domain and addr:
|
if not current_address in domain and addr:
|
||||||
self.parent.set_receive_address(addr)
|
self.parent.set_receive_address(addr)
|
||||||
self.parent.new_request_button.setEnabled(addr != current_address)
|
self.parent.new_request_button.setEnabled(addr != current_address)
|
||||||
|
@ -98,11 +94,10 @@ class RequestList(MyTreeWidget):
|
||||||
signature = req.get('sig')
|
signature = req.get('sig')
|
||||||
requestor = req.get('name', '')
|
requestor = req.get('name', '')
|
||||||
amount_str = self.parent.format_amount(amount) if amount else ""
|
amount_str = self.parent.format_amount(amount) if amount else ""
|
||||||
account = ''
|
item = QTreeWidgetItem([date, address, '', message, amount_str, pr_tooltips.get(status,'')])
|
||||||
item = QTreeWidgetItem([date, account, address, '', message, amount_str, pr_tooltips.get(status,'')])
|
|
||||||
if signature is not None:
|
if signature is not None:
|
||||||
item.setIcon(3, QIcon(":icons/seal.png"))
|
item.setIcon(2, QIcon(":icons/seal.png"))
|
||||||
item.setToolTip(3, 'signed by '+ requestor)
|
item.setToolTip(2, 'signed by '+ requestor)
|
||||||
if status is not PR_UNKNOWN:
|
if status is not PR_UNKNOWN:
|
||||||
item.setIcon(6, QIcon(pr_icons.get(status)))
|
item.setIcon(6, QIcon(pr_icons.get(status)))
|
||||||
self.addTopLevelItem(item)
|
self.addTopLevelItem(item)
|
||||||
|
|
|
@ -39,19 +39,13 @@ def icon_filename(sid):
|
||||||
return ":icons/seed.png"
|
return ":icons/seed.png"
|
||||||
|
|
||||||
class SeedDialog(WindowModalDialog):
|
class SeedDialog(WindowModalDialog):
|
||||||
def __init__(self, parent, seed, imported_keys):
|
def __init__(self, parent, seed):
|
||||||
WindowModalDialog.__init__(self, parent, ('Electrum - ' + _('Seed')))
|
WindowModalDialog.__init__(self, parent, ('Electrum - ' + _('Seed')))
|
||||||
self.setMinimumWidth(400)
|
self.setMinimumWidth(400)
|
||||||
vbox = QVBoxLayout(self)
|
vbox = QVBoxLayout(self)
|
||||||
vbox.addLayout(SeedWarningLayout(seed).layout())
|
vbox.addLayout(SeedWarningLayout(seed).layout())
|
||||||
if imported_keys:
|
|
||||||
warning = ("<b>" + _("WARNING") + ":</b> " +
|
|
||||||
_("Your wallet contains imported keys. These keys "
|
|
||||||
"cannot be recovered from your seed.") + "</b><p>")
|
|
||||||
vbox.addWidget(WWLabel(warning))
|
|
||||||
vbox.addLayout(Buttons(CloseButton(self)))
|
vbox.addLayout(Buttons(CloseButton(self)))
|
||||||
|
|
||||||
|
|
||||||
class SeedLayoutBase(object):
|
class SeedLayoutBase(object):
|
||||||
def _seed_layout(self, seed=None, title=None, sid=None):
|
def _seed_layout(self, seed=None, title=None, sid=None):
|
||||||
logo = QLabel()
|
logo = QLabel()
|
||||||
|
|
381
lib/account.py
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,14 +24,10 @@
|
||||||
# SOFTWARE.
|
# SOFTWARE.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from electrum.wallet import Wallet, Multisig_Wallet, WalletStorage
|
import keystore
|
||||||
|
from wallet import Wallet, Imported_Wallet, Standard_Wallet, Multisig_Wallet, WalletStorage
|
||||||
from i18n import _
|
from i18n import _
|
||||||
|
from plugins import run_hook
|
||||||
|
|
||||||
is_any_key = lambda x: Wallet.is_old_mpk(x) or Wallet.is_xprv(x) or Wallet.is_xpub(x) or Wallet.is_address(x) or Wallet.is_private_key(x)
|
|
||||||
is_private_key = lambda x: Wallet.is_xprv(x) or Wallet.is_private_key(x)
|
|
||||||
is_bip32_key = lambda x: Wallet.is_xprv(x) or Wallet.is_xpub(x)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseWizard(object):
|
class BaseWizard(object):
|
||||||
|
|
||||||
|
@ -42,6 +38,7 @@ class BaseWizard(object):
|
||||||
self.storage = WalletStorage(path)
|
self.storage = WalletStorage(path)
|
||||||
self.wallet = None
|
self.wallet = None
|
||||||
self.stack = []
|
self.stack = []
|
||||||
|
self.plugin = None
|
||||||
|
|
||||||
def run(self, *args):
|
def run(self, *args):
|
||||||
action = args[0]
|
action = args[0]
|
||||||
|
@ -49,27 +46,17 @@ class BaseWizard(object):
|
||||||
self.stack.append((action, args))
|
self.stack.append((action, args))
|
||||||
if not action:
|
if not action:
|
||||||
return
|
return
|
||||||
if hasattr(self.wallet, 'plugin') and hasattr(self.wallet.plugin, action):
|
if type(action) is tuple:
|
||||||
f = getattr(self.wallet.plugin, action)
|
self.plugin, action = action
|
||||||
apply(f, (self.wallet, self) + args)
|
if self.plugin and hasattr(self.plugin, action):
|
||||||
|
f = getattr(self.plugin, action)
|
||||||
|
apply(f, (self,) + args)
|
||||||
elif hasattr(self, action):
|
elif hasattr(self, action):
|
||||||
f = getattr(self, action)
|
f = getattr(self, action)
|
||||||
apply(f, args)
|
apply(f, args)
|
||||||
else:
|
else:
|
||||||
raise BaseException("unknown action", action)
|
raise BaseException("unknown action", action)
|
||||||
|
|
||||||
def get_action(self):
|
|
||||||
if self.storage.file_exists:
|
|
||||||
self.wallet = Wallet(self.storage)
|
|
||||||
action = self.wallet.get_action()
|
|
||||||
else:
|
|
||||||
action = 'new'
|
|
||||||
return action
|
|
||||||
|
|
||||||
def get_wallet(self):
|
|
||||||
if self.wallet and self.wallet.get_action() is None:
|
|
||||||
return self.wallet
|
|
||||||
|
|
||||||
def can_go_back(self):
|
def can_go_back(self):
|
||||||
return len(self.stack)>1
|
return len(self.stack)>1
|
||||||
|
|
||||||
|
@ -91,10 +78,9 @@ class BaseWizard(object):
|
||||||
('standard', _("Standard wallet")),
|
('standard', _("Standard wallet")),
|
||||||
('twofactor', _("Wallet with two-factor authentication")),
|
('twofactor', _("Wallet with two-factor authentication")),
|
||||||
('multisig', _("Multi-signature wallet")),
|
('multisig', _("Multi-signature wallet")),
|
||||||
('hardware', _("Hardware wallet")),
|
|
||||||
]
|
]
|
||||||
registered_kinds = Wallet.categories()
|
registered_kinds = Wallet.categories()
|
||||||
choices = [pair for pair in wallet_kinds if pair[0] in registered_kinds]
|
choices = wallet_kinds#[pair for pair in wallet_kinds if pair[0] in registered_kinds]
|
||||||
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.on_wallet_type)
|
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.on_wallet_type)
|
||||||
|
|
||||||
def on_wallet_type(self, choice):
|
def on_wallet_type(self, choice):
|
||||||
|
@ -103,66 +89,58 @@ class BaseWizard(object):
|
||||||
action = 'choose_seed'
|
action = 'choose_seed'
|
||||||
elif choice == 'multisig':
|
elif choice == 'multisig':
|
||||||
action = 'choose_multisig'
|
action = 'choose_multisig'
|
||||||
elif choice == 'hardware':
|
|
||||||
action = 'choose_hw'
|
|
||||||
elif choice == 'twofactor':
|
elif choice == 'twofactor':
|
||||||
action = 'choose_seed'
|
self.storage.put('wallet_type', '2fa')
|
||||||
|
self.storage.put('use_trustedcoin', True)
|
||||||
|
self.plugin = self.plugins.load_plugin('trustedcoin')
|
||||||
|
action = self.storage.get_action()
|
||||||
|
|
||||||
self.run(action)
|
self.run(action)
|
||||||
|
|
||||||
def choose_multisig(self):
|
def choose_multisig(self):
|
||||||
def on_multisig(m, n):
|
def on_multisig(m, n):
|
||||||
self.multisig_type = "%dof%d"%(m, n)
|
self.multisig_type = "%dof%d"%(m, n)
|
||||||
|
self.n = n
|
||||||
self.run('choose_seed')
|
self.run('choose_seed')
|
||||||
self.multisig_dialog(run_next=on_multisig)
|
self.multisig_dialog(run_next=on_multisig)
|
||||||
|
|
||||||
def choose_seed(self):
|
def choose_seed(self):
|
||||||
title = _('Choose Seed')
|
title = _('Seed and Private Keys')
|
||||||
message = _("Do you want to create a new seed, or to restore a wallet using an existing seed?")
|
message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?')
|
||||||
if self.wallet_type == 'standard':
|
if self.wallet_type in ['standard', 'multisig']:
|
||||||
choices = [
|
choices = [
|
||||||
('create_seed', _('Create a new seed')),
|
('create_seed', _('Create a new seed')),
|
||||||
('restore_seed', _('I already have a seed')),
|
('restore_seed', _('I already have a seed')),
|
||||||
('restore_from_key', _('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)
|
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
|
||||||
|
|
||||||
def create_2fa(self):
|
|
||||||
self.storage.put('wallet_type', '2fa')
|
|
||||||
self.wallet = Wallet(self.storage)
|
|
||||||
self.run('show_disclaimer')
|
|
||||||
|
|
||||||
def restore_seed(self):
|
def restore_seed(self):
|
||||||
# TODO: return derivation password too
|
# TODO: return derivation password too
|
||||||
self.restore_seed_dialog(run_next=self.add_password, is_valid=Wallet.is_seed)
|
self.restore_seed_dialog(run_next=self.add_password, is_valid=keystore.is_seed)
|
||||||
|
|
||||||
def on_restore(self, text):
|
def on_restore(self, text):
|
||||||
if is_private_key(text):
|
if keystore.is_address_list(text):
|
||||||
|
self.wallet = Imported_Wallet(self.storage)
|
||||||
|
for x in text.split():
|
||||||
|
self.wallet.add_address(x)
|
||||||
|
self.terminate()
|
||||||
|
elif keystore.is_private(text):
|
||||||
self.add_password(text)
|
self.add_password(text)
|
||||||
else:
|
else:
|
||||||
self.create_wallet(text, None)
|
self.create_keystore(text, None)
|
||||||
|
|
||||||
def restore_from_key(self):
|
def restore_from_key(self):
|
||||||
if self.wallet_type == 'standard':
|
if self.wallet_type == 'standard':
|
||||||
v = is_any_key
|
v = keystore.is_any_key
|
||||||
title = _("Import keys")
|
title = _("Import keys")
|
||||||
message = ' '.join([
|
message = ' '.join([
|
||||||
_("To create a watching-only wallet, please enter your master public key (xpub), or a list of Bitcoin addresses."),
|
_("To create a watching-only wallet, please enter your master public key (xpub), or a list of Bitcoin addresses."),
|
||||||
_("To create a spending wallet, please enter a master private key (xprv), or a list of Bitcoin private keys.")
|
_("To create a spending wallet, please enter a master private key (xprv), or a list of Bitcoin private keys.")
|
||||||
])
|
])
|
||||||
else:
|
else:
|
||||||
v = is_bip32_key
|
v = keystore.is_bip32_key
|
||||||
title = _("Master public or private key")
|
title = _("Master public or private key")
|
||||||
message = ' '.join([
|
message = ' '.join([
|
||||||
_("To create a watching-only wallet, please enter your master public key (xpub)."),
|
_("To create a watching-only wallet, please enter your master public key (xpub)."),
|
||||||
|
@ -170,12 +148,8 @@ class BaseWizard(object):
|
||||||
])
|
])
|
||||||
self.restore_keys_dialog(title=title, message=message, run_next=self.on_restore, is_valid=v)
|
self.restore_keys_dialog(title=title, message=message, run_next=self.on_restore, is_valid=v)
|
||||||
|
|
||||||
def restore_2fa(self):
|
|
||||||
self.storage.put('wallet_type', '2fa')
|
|
||||||
self.wallet = Wallet(self.storage)
|
|
||||||
self.wallet.plugin.on_restore_wallet(self.wallet, self)
|
|
||||||
|
|
||||||
def choose_hw(self):
|
def choose_hw(self):
|
||||||
|
self.storage.put('key_type', 'hardware')
|
||||||
hw_wallet_types, choices = self.plugins.hardware_wallets('create')
|
hw_wallet_types, choices = self.plugins.hardware_wallets('create')
|
||||||
choices = zip(hw_wallet_types, choices)
|
choices = zip(hw_wallet_types, choices)
|
||||||
title = _('Hardware wallet')
|
title = _('Hardware wallet')
|
||||||
|
@ -189,84 +163,87 @@ class BaseWizard(object):
|
||||||
self.choice_dialog(title=title, message=msg, choices=choices, run_next=self.on_hardware)
|
self.choice_dialog(title=title, message=msg, choices=choices, run_next=self.on_hardware)
|
||||||
|
|
||||||
def on_hardware(self, hw_type):
|
def on_hardware(self, hw_type):
|
||||||
self.hw_type = hw_type
|
self.storage.put('hardware_type', hw_type)
|
||||||
if self.wallet_type == 'multisig':
|
|
||||||
self.create_hardware_multisig()
|
|
||||||
else:
|
|
||||||
title = _('Hardware wallet') + ' [%s]' % 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?')
|
message = _('Do you have a device, or do you want to restore a wallet using an existing seed?')
|
||||||
choices = [
|
choices = [
|
||||||
('create_hardware_wallet', _('I have a device')),
|
('on_hardware_device', _('I have a %s device')%hw_type),
|
||||||
('restore_hardware_wallet', _('Use hardware wallet seed')),
|
('on_hardware_seed', _('I have a %s seed')%hw_type),
|
||||||
]
|
]
|
||||||
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
|
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
|
||||||
|
|
||||||
def create_hardware_multisig(self):
|
def on_hardware_device(self):
|
||||||
self.storage.put('wallet_type', self.multisig_type)
|
from keystore import load_keystore
|
||||||
self.wallet = Multisig_Wallet(self.storage)
|
keystore = load_keystore(self.storage, None)
|
||||||
# todo: get the xpub from the plugin
|
keystore.plugin.on_create_wallet(keystore, self)
|
||||||
self.run('create_wallet', xpub, None)
|
self.create_wallet(keystore, None)
|
||||||
|
|
||||||
def create_hardware_wallet(self):
|
def on_hardware_seed(self):
|
||||||
self.storage.put('wallet_type', self.hw_type)
|
from keystore import load_keystore
|
||||||
self.wallet = Wallet(self.storage)
|
self.storage.put('key_type', 'hw_seed')
|
||||||
self.wallet.plugin.on_create_wallet(self.wallet, self)
|
keystore = load_keystore(self.storage, None)
|
||||||
self.terminate()
|
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):
|
def create_wallet(self, k, password):
|
||||||
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):
|
|
||||||
if self.wallet_type == 'standard':
|
if self.wallet_type == 'standard':
|
||||||
self.wallet = Wallet.from_text(text, password, self.storage)
|
k.save(self.storage, 'x/')
|
||||||
|
self.wallet = Standard_Wallet(self.storage)
|
||||||
self.run('create_addresses')
|
self.run('create_addresses')
|
||||||
elif self.wallet_type == 'multisig':
|
elif self.wallet_type == 'multisig':
|
||||||
self.storage.put('wallet_type', self.multisig_type)
|
self.storage.put('wallet_type', self.multisig_type)
|
||||||
self.wallet = Multisig_Wallet(self.storage)
|
self.add_cosigner(k, 0)
|
||||||
self.wallet.add_cosigner('x1/', text, password)
|
xpub = k.get_master_public_key()
|
||||||
self.stack = []
|
self.stack = []
|
||||||
self.run('show_xpub_and_add_cosigners', (password,))
|
self.run('show_xpub_and_add_cosigners', password, xpub)
|
||||||
|
|
||||||
def show_xpub_and_add_cosigners(self, password):
|
def show_xpub_and_add_cosigners(self, password, xpub):
|
||||||
xpub = self.wallet.master_public_keys.get('x1/')
|
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('add_cosigners', password, 1))
|
||||||
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('add_cosigners', password))
|
|
||||||
|
|
||||||
def add_cosigners(self, password):
|
def add_cosigner(self, keystore, i):
|
||||||
i = self.wallet.get_missing_cosigner()
|
d = self.storage.get('master_public_keys', {})
|
||||||
self.add_cosigner_dialog(run_next=lambda x: self.on_cosigner(x, password), index=(i-1), is_valid=Wallet.is_xpub)
|
if keystore.xpub in d.values():
|
||||||
|
raise BaseException('duplicate key')
|
||||||
|
keystore.save(self.storage, 'x%d/'%(i+1))
|
||||||
|
|
||||||
def on_cosigner(self, text, password):
|
def add_cosigners(self, password, i):
|
||||||
i = self.wallet.get_missing_cosigner()
|
self.add_cosigner_dialog(run_next=lambda x: self.on_cosigner(x, password, i), index=i, is_valid=keystore.is_xpub)
|
||||||
|
|
||||||
|
def on_cosigner(self, text, password, i):
|
||||||
|
k = keystore.from_text(text, password)
|
||||||
try:
|
try:
|
||||||
self.wallet.add_cosigner('x%d/'%i, text, password)
|
self.add_cosigner(k, i)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
print "error:" + str(e)
|
self.show_message("error:" + str(e))
|
||||||
i = self.wallet.get_missing_cosigner()
|
return
|
||||||
if i:
|
if i < self.n - 1:
|
||||||
self.run('add_cosigners', password)
|
self.run('add_cosigners', password, i+1)
|
||||||
else:
|
else:
|
||||||
|
self.wallet = Multisig_Wallet(self.storage)
|
||||||
self.create_addresses()
|
self.create_addresses()
|
||||||
|
|
||||||
def create_addresses(self):
|
|
||||||
def task():
|
|
||||||
self.wallet.create_main_account()
|
|
||||||
self.wallet.synchronize()
|
|
||||||
self.wallet.storage.write()
|
|
||||||
self.terminate()
|
|
||||||
msg = _("Electrum is generating your addresses, please wait.")
|
|
||||||
self.waiting_dialog(task, msg)
|
|
||||||
|
|
||||||
def create_seed(self):
|
def create_seed(self):
|
||||||
from electrum.wallet import BIP32_Wallet
|
from electrum.mnemonic import Mnemonic
|
||||||
seed = BIP32_Wallet.make_seed()
|
seed = Mnemonic('en').make_seed()
|
||||||
self.show_seed_dialog(run_next=self.confirm_seed, seed_text=seed)
|
self.show_seed_dialog(run_next=self.confirm_seed, seed_text=seed)
|
||||||
|
|
||||||
def confirm_seed(self, seed):
|
def confirm_seed(self, seed):
|
||||||
self.confirm_seed_dialog(run_next=self.add_password, is_valid=lambda x: x==seed)
|
self.confirm_seed_dialog(run_next=self.add_password, is_valid=lambda x: x==seed)
|
||||||
|
|
||||||
def add_password(self, text):
|
def add_password(self, text):
|
||||||
f = lambda pw: self.run('create_wallet', text, pw)
|
f = lambda pw: self.run('create_keystore', text, pw)
|
||||||
self.request_password(run_next=f)
|
self.request_password(run_next=f)
|
||||||
|
|
||||||
|
def create_keystore(self, text, password):
|
||||||
|
k = keystore.from_text(text, password)
|
||||||
|
self.create_wallet(k, password)
|
||||||
|
|
||||||
|
def create_addresses(self):
|
||||||
|
def task():
|
||||||
|
self.wallet.synchronize()
|
||||||
|
self.wallet.storage.write()
|
||||||
|
self.terminate()
|
||||||
|
msg = _("Electrum is generating your addresses, please wait.")
|
||||||
|
self.waiting_dialog(task, msg)
|
||||||
|
|
|
@ -300,12 +300,9 @@ class Commands:
|
||||||
return self.wallet.get_public_keys(address)
|
return self.wallet.get_public_keys(address)
|
||||||
|
|
||||||
@command('w')
|
@command('w')
|
||||||
def getbalance(self, account=None):
|
def getbalance(self):
|
||||||
"""Return the balance of your wallet. """
|
"""Return the balance of your wallet. """
|
||||||
if account is None:
|
|
||||||
c, u, x = self.wallet.get_balance()
|
c, u, x = self.wallet.get_balance()
|
||||||
else:
|
|
||||||
c, u, x = self.wallet.get_account_balance(account)
|
|
||||||
out = {"confirmed": str(Decimal(c)/COIN)}
|
out = {"confirmed": str(Decimal(c)/COIN)}
|
||||||
if u:
|
if u:
|
||||||
out["unconfirmed"] = str(Decimal(u)/COIN)
|
out["unconfirmed"] = str(Decimal(u)/COIN)
|
||||||
|
@ -357,7 +354,7 @@ class Commands:
|
||||||
@command('wp')
|
@command('wp')
|
||||||
def getmasterprivate(self):
|
def getmasterprivate(self):
|
||||||
"""Get master private key. Return your wallet\'s master private key"""
|
"""Get master private key. Return your wallet\'s master private key"""
|
||||||
return str(self.wallet.get_master_private_key(self.wallet.root_name, self._password))
|
return str(self.wallet.keystore.get_master_private_key(self._password))
|
||||||
|
|
||||||
@command('wp')
|
@command('wp')
|
||||||
def getseed(self):
|
def getseed(self):
|
||||||
|
@ -499,7 +496,7 @@ class Commands:
|
||||||
def listaddresses(self, receiving=False, change=False, show_labels=False, frozen=False, unused=False, funded=False, show_balance=False):
|
def listaddresses(self, receiving=False, change=False, show_labels=False, frozen=False, unused=False, funded=False, show_balance=False):
|
||||||
"""List wallet addresses. Returns the list of all addresses in your wallet. Use optional arguments to filter the results."""
|
"""List wallet addresses. Returns the list of all addresses in your wallet. Use optional arguments to filter the results."""
|
||||||
out = []
|
out = []
|
||||||
for addr in self.wallet.addresses(True):
|
for addr in self.wallet.get_addresses():
|
||||||
if frozen and not self.wallet.is_frozen(addr):
|
if frozen and not self.wallet.is_frozen(addr):
|
||||||
continue
|
continue
|
||||||
if receiving and self.wallet.is_change(addr):
|
if receiving and self.wallet.is_change(addr):
|
||||||
|
@ -681,7 +678,6 @@ command_options = {
|
||||||
'unsigned': ("-u", "--unsigned", "Do not sign transaction"),
|
'unsigned': ("-u", "--unsigned", "Do not sign transaction"),
|
||||||
'rbf': (None, "--rbf", "Replace-by-fee transaction"),
|
'rbf': (None, "--rbf", "Replace-by-fee transaction"),
|
||||||
'domain': ("-D", "--domain", "List of addresses"),
|
'domain': ("-D", "--domain", "List of addresses"),
|
||||||
'account': (None, "--account", "Account"),
|
|
||||||
'memo': ("-m", "--memo", "Description of the request"),
|
'memo': ("-m", "--memo", "Description of the request"),
|
||||||
'expiration': (None, "--expiration", "Time in seconds"),
|
'expiration': (None, "--expiration", "Time in seconds"),
|
||||||
'timeout': (None, "--timeout", "Timeout in seconds"),
|
'timeout': (None, "--timeout", "Timeout in seconds"),
|
||||||
|
|
|
@ -37,7 +37,7 @@ from util import print_msg, print_error, print_stderr
|
||||||
from wallet import WalletStorage, Wallet
|
from wallet import WalletStorage, Wallet
|
||||||
from commands import known_commands, Commands
|
from commands import known_commands, Commands
|
||||||
from simple_config import SimpleConfig
|
from simple_config import SimpleConfig
|
||||||
|
from plugins import run_hook
|
||||||
|
|
||||||
def get_lockfile(config):
|
def get_lockfile(config):
|
||||||
return os.path.join(config.path, 'daemon')
|
return os.path.join(config.path, 'daemon')
|
||||||
|
@ -171,16 +171,16 @@ class Daemon(DaemonThread):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def load_wallet(self, path):
|
def load_wallet(self, path):
|
||||||
|
# wizard will be launched if we return
|
||||||
if path in self.wallets:
|
if path in self.wallets:
|
||||||
wallet = self.wallets[path]
|
wallet = self.wallets[path]
|
||||||
return wallet
|
return wallet
|
||||||
storage = WalletStorage(path)
|
storage = WalletStorage(path)
|
||||||
if not storage.file_exists:
|
if not storage.file_exists:
|
||||||
return
|
return
|
||||||
wallet = Wallet(storage)
|
if storage.requires_split() or storage.requires_upgrade() or storage.get_action():
|
||||||
action = wallet.get_action()
|
|
||||||
if action:
|
|
||||||
return
|
return
|
||||||
|
wallet = Wallet(storage)
|
||||||
wallet.start_threads(self.network)
|
wallet.start_threads(self.network)
|
||||||
self.wallets[path] = wallet
|
self.wallets[path] = wallet
|
||||||
return wallet
|
return wallet
|
||||||
|
|
701
lib/keystore.py
Normal file
701
lib/keystore.py
Normal file
|
@ -0,0 +1,701 @@
|
||||||
|
#!/usr/bin/env python2
|
||||||
|
# -*- mode: python -*-
|
||||||
|
#
|
||||||
|
# Electrum - lightweight Bitcoin client
|
||||||
|
# Copyright (C) 2016 The Electrum developers
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person
|
||||||
|
# obtaining a copy of this software and associated documentation files
|
||||||
|
# (the "Software"), to deal in the Software without restriction,
|
||||||
|
# including without limitation the rights to use, copy, modify, merge,
|
||||||
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
|
# and to permit persons to whom the Software is furnished to do so,
|
||||||
|
# subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
from unicodedata import normalize
|
||||||
|
|
||||||
|
from version import *
|
||||||
|
import bitcoin
|
||||||
|
from bitcoin import pw_encode, pw_decode, bip32_root, bip32_private_derivation, bip32_public_derivation, bip32_private_key, deserialize_xkey
|
||||||
|
from bitcoin import public_key_from_private_key, public_key_to_bc_address
|
||||||
|
from bitcoin import *
|
||||||
|
|
||||||
|
from bitcoin import is_old_seed, is_new_seed
|
||||||
|
from util import PrintError, InvalidPassword
|
||||||
|
from mnemonic import Mnemonic
|
||||||
|
|
||||||
|
|
||||||
|
class KeyStore(PrintError):
|
||||||
|
|
||||||
|
def has_seed(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_password(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_watching_only(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_import(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class Software_KeyStore(KeyStore):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
KeyStore.__init__(self)
|
||||||
|
self.use_encryption = False
|
||||||
|
|
||||||
|
def has_password(self):
|
||||||
|
return self.use_encryption
|
||||||
|
|
||||||
|
|
||||||
|
class Imported_KeyStore(Software_KeyStore):
|
||||||
|
# keystore for imported private keys
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
Software_KeyStore.__init__(self)
|
||||||
|
self.keypairs = {}
|
||||||
|
|
||||||
|
def is_deterministic(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_change_password(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_master_public_key(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def load(self, storage, name):
|
||||||
|
self.keypairs = storage.get('keypairs', {})
|
||||||
|
self.use_encryption = storage.get('use_encryption', False)
|
||||||
|
self.receiving_pubkeys = self.keypairs.keys()
|
||||||
|
self.change_pubkeys = []
|
||||||
|
|
||||||
|
def save(self, storage, root_name):
|
||||||
|
storage.put('key_type', 'imported')
|
||||||
|
storage.put('keypairs', self.keypairs)
|
||||||
|
storage.put('use_encryption', self.use_encryption)
|
||||||
|
|
||||||
|
def can_import(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
self.get_private_key((0,0), password)
|
||||||
|
|
||||||
|
def import_key(self, sec, password):
|
||||||
|
if not self.can_import():
|
||||||
|
raise BaseException('This wallet cannot import private keys')
|
||||||
|
try:
|
||||||
|
pubkey = public_key_from_private_key(sec)
|
||||||
|
except Exception:
|
||||||
|
raise Exception('Invalid private key')
|
||||||
|
self.keypairs[pubkey] = sec
|
||||||
|
return pubkey
|
||||||
|
|
||||||
|
def delete_imported_key(self, key):
|
||||||
|
self.keypairs.pop(key)
|
||||||
|
|
||||||
|
def get_private_key(self, sequence, password):
|
||||||
|
for_change, i = sequence
|
||||||
|
assert for_change == 0
|
||||||
|
pubkey = (self.change_pubkeys if for_change else self.receiving_pubkeys)[i]
|
||||||
|
pk = pw_decode(self.keypairs[pubkey], password)
|
||||||
|
# this checks the password
|
||||||
|
if pubkey != public_key_from_private_key(pk):
|
||||||
|
raise InvalidPassword()
|
||||||
|
return pk
|
||||||
|
|
||||||
|
def update_password(self, old_password, new_password):
|
||||||
|
if old_password is not None:
|
||||||
|
self.check_password(old_password)
|
||||||
|
if new_password == '':
|
||||||
|
new_password = None
|
||||||
|
for k, v in self.keypairs.items():
|
||||||
|
b = pw_decode(v, old_password)
|
||||||
|
c = pw_encode(b, new_password)
|
||||||
|
self.keypairs[k] = b
|
||||||
|
self.use_encryption = (new_password is not None)
|
||||||
|
|
||||||
|
|
||||||
|
class Deterministic_KeyStore(Software_KeyStore):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
Software_KeyStore.__init__(self)
|
||||||
|
self.seed = ''
|
||||||
|
|
||||||
|
def is_deterministic(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def load(self, storage, name):
|
||||||
|
self.seed = storage.get('seed', '')
|
||||||
|
self.use_encryption = storage.get('use_encryption', False)
|
||||||
|
|
||||||
|
def save(self, storage, name):
|
||||||
|
storage.put('seed', self.seed)
|
||||||
|
storage.put('use_encryption', self.use_encryption)
|
||||||
|
|
||||||
|
def has_seed(self):
|
||||||
|
return self.seed != ''
|
||||||
|
|
||||||
|
def can_change_password(self):
|
||||||
|
return not self.is_watching_only()
|
||||||
|
|
||||||
|
def add_seed(self, seed, password):
|
||||||
|
if self.seed:
|
||||||
|
raise Exception("a seed exists")
|
||||||
|
self.seed_version, self.seed = self.format_seed(seed)
|
||||||
|
if password:
|
||||||
|
self.seed = pw_encode(self.seed, password)
|
||||||
|
self.use_encryption = (password is not None)
|
||||||
|
|
||||||
|
def get_seed(self, password):
|
||||||
|
return pw_decode(self.seed, password).encode('utf8')
|
||||||
|
|
||||||
|
|
||||||
|
class Xpub:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.xpub = None
|
||||||
|
self.xpub_receive = None
|
||||||
|
self.xpub_change = None
|
||||||
|
|
||||||
|
def add_master_public_key(self, xpub):
|
||||||
|
self.xpub = xpub
|
||||||
|
|
||||||
|
def get_master_public_key(self):
|
||||||
|
return self.xpub
|
||||||
|
|
||||||
|
def derive_pubkey(self, for_change, n):
|
||||||
|
xpub = self.xpub_change if for_change else self.xpub_receive
|
||||||
|
if xpub is None:
|
||||||
|
xpub = bip32_public_derivation(self.xpub, "", "/%d"%for_change)
|
||||||
|
if for_change:
|
||||||
|
self.xpub_change = xpub
|
||||||
|
else:
|
||||||
|
self.xpub_receive = xpub
|
||||||
|
_, _, _, c, cK = deserialize_xkey(xpub)
|
||||||
|
cK, c = CKD_pub(cK, c, n)
|
||||||
|
result = cK.encode('hex')
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_xpubkey(self, c, i):
|
||||||
|
s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (c, i)))
|
||||||
|
return 'ff' + bitcoin.DecodeBase58Check(self.xpub).encode('hex') + s
|
||||||
|
|
||||||
|
|
||||||
|
class BIP32_KeyStore(Deterministic_KeyStore, Xpub):
|
||||||
|
root_derivation = "m/"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
Xpub.__init__(self)
|
||||||
|
Deterministic_KeyStore.__init__(self)
|
||||||
|
self.xprv = None
|
||||||
|
|
||||||
|
def format_seed(self, seed):
|
||||||
|
return NEW_SEED_VERSION, ' '.join(seed.split())
|
||||||
|
|
||||||
|
def load(self, storage, name):
|
||||||
|
Deterministic_KeyStore.load(self, storage, name)
|
||||||
|
self.xpub = storage.get('master_public_keys', {}).get(name)
|
||||||
|
self.xprv = storage.get('master_private_keys', {}).get(name)
|
||||||
|
|
||||||
|
def save(self, storage, name):
|
||||||
|
Deterministic_KeyStore.save(self, storage, name)
|
||||||
|
d = storage.get('master_public_keys', {})
|
||||||
|
d[name] = self.xpub
|
||||||
|
storage.put('master_public_keys', d)
|
||||||
|
d = storage.get('master_private_keys', {})
|
||||||
|
d[name] = self.xprv
|
||||||
|
storage.put('master_private_keys', d)
|
||||||
|
|
||||||
|
def add_master_private_key(self, xprv, password):
|
||||||
|
self.xprv = pw_encode(xprv, password)
|
||||||
|
|
||||||
|
def get_master_private_key(self, password):
|
||||||
|
return pw_decode(self.xprv, password)
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
xprv = pw_decode(self.xprv, password)
|
||||||
|
if deserialize_xkey(xprv)[3] != deserialize_xkey(self.xpub)[3]:
|
||||||
|
raise InvalidPassword()
|
||||||
|
|
||||||
|
def update_password(self, old_password, new_password):
|
||||||
|
if old_password is not None:
|
||||||
|
self.check_password(old_password)
|
||||||
|
if new_password == '':
|
||||||
|
new_password = None
|
||||||
|
if self.has_seed():
|
||||||
|
decoded = self.get_seed(old_password)
|
||||||
|
self.seed = pw_encode( decoded, new_password)
|
||||||
|
if self.xprv is not None:
|
||||||
|
b = pw_decode(self.xprv, old_password)
|
||||||
|
self.xprv = pw_encode(b, new_password)
|
||||||
|
self.use_encryption = (new_password is not None)
|
||||||
|
|
||||||
|
def is_watching_only(self):
|
||||||
|
return self.xprv is None
|
||||||
|
|
||||||
|
def get_keypairs_for_sig(self, tx, password):
|
||||||
|
keypairs = {}
|
||||||
|
for txin in tx.inputs():
|
||||||
|
num_sig = txin.get('num_sig')
|
||||||
|
if num_sig is None:
|
||||||
|
continue
|
||||||
|
x_signatures = txin['signatures']
|
||||||
|
signatures = filter(None, x_signatures)
|
||||||
|
if len(signatures) == num_sig:
|
||||||
|
# input is complete
|
||||||
|
continue
|
||||||
|
for k, x_pubkey in enumerate(txin['x_pubkeys']):
|
||||||
|
if x_signatures[k] is not None:
|
||||||
|
# this pubkey already signed
|
||||||
|
continue
|
||||||
|
derivation = txin['derivation']
|
||||||
|
sec = self.get_private_key(derivation, password)
|
||||||
|
if sec:
|
||||||
|
keypairs[x_pubkey] = sec
|
||||||
|
|
||||||
|
return keypairs
|
||||||
|
|
||||||
|
def sign_transaction(self, tx, password):
|
||||||
|
# Raise if password is not correct.
|
||||||
|
self.check_password(password)
|
||||||
|
# Add private keys
|
||||||
|
keypairs = self.get_keypairs_for_sig(tx, password)
|
||||||
|
# Sign
|
||||||
|
if keypairs:
|
||||||
|
tx.sign(keypairs)
|
||||||
|
|
||||||
|
def derive_xkeys(self, root, derivation, password):
|
||||||
|
x = self.master_private_keys[root]
|
||||||
|
root_xprv = pw_decode(x, password)
|
||||||
|
xprv, xpub = bip32_private_derivation(root_xprv, root, derivation)
|
||||||
|
return xpub, xprv
|
||||||
|
|
||||||
|
def get_mnemonic(self, password):
|
||||||
|
return self.get_seed(password)
|
||||||
|
|
||||||
|
def mnemonic_to_seed(self, seed, password):
|
||||||
|
return Mnemonic.mnemonic_to_seed(seed, password)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def make_seed(self, lang=None):
|
||||||
|
return Mnemonic(lang).make_seed()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def address_derivation(self, account_id, change, address_index):
|
||||||
|
account_derivation = self.account_derivation(account_id)
|
||||||
|
return "%s/%d/%d" % (account_derivation, change, address_index)
|
||||||
|
|
||||||
|
def address_id(self, address):
|
||||||
|
acc_id, (change, address_index) = self.get_address_index(address)
|
||||||
|
return self.address_derivation(acc_id, change, address_index)
|
||||||
|
|
||||||
|
def add_seed_and_xprv(self, seed, password, passphrase=''):
|
||||||
|
xprv, xpub = bip32_root(self.mnemonic_to_seed(seed, passphrase))
|
||||||
|
xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation)
|
||||||
|
self.add_seed(seed, password)
|
||||||
|
self.add_master_private_key(xprv, password)
|
||||||
|
self.add_master_public_key(xpub)
|
||||||
|
|
||||||
|
def add_xprv(self, xprv, password):
|
||||||
|
xpub = bitcoin.xpub_from_xprv(xprv)
|
||||||
|
self.add_master_private_key(xprv, password)
|
||||||
|
self.add_master_public_key(xpub)
|
||||||
|
|
||||||
|
def can_sign(self, xpub):
|
||||||
|
return xpub == self.xpub and self.xprv is not None
|
||||||
|
|
||||||
|
def get_private_key(self, sequence, password):
|
||||||
|
xprv = self.get_master_private_key(password)
|
||||||
|
_, _, _, c, k = deserialize_xkey(xprv)
|
||||||
|
pk = bip32_private_key(sequence, k, c)
|
||||||
|
return pk
|
||||||
|
|
||||||
|
|
||||||
|
class Old_KeyStore(Deterministic_KeyStore):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
Deterministic_KeyStore.__init__(self)
|
||||||
|
self.mpk = None
|
||||||
|
|
||||||
|
def load(self, storage, name):
|
||||||
|
Deterministic_KeyStore.load(self, storage, name)
|
||||||
|
self.mpk = storage.get('master_public_key').decode('hex')
|
||||||
|
|
||||||
|
def save(self, storage, name):
|
||||||
|
Deterministic_KeyStore.save(self, storage, name)
|
||||||
|
storage.put('wallet_type', 'old')
|
||||||
|
storage.put('master_public_key', self.mpk.encode('hex'))
|
||||||
|
|
||||||
|
def add_seed(self, seed, password):
|
||||||
|
Deterministic_KeyStore.add_seed(self, seed, password)
|
||||||
|
self.mpk = self.mpk_from_seed(self.get_seed(password))
|
||||||
|
|
||||||
|
def add_master_public_key(self, mpk):
|
||||||
|
self.mpk = mpk.decode('hex')
|
||||||
|
|
||||||
|
def format_seed(self, seed):
|
||||||
|
import old_mnemonic
|
||||||
|
# see if seed was entered as hex
|
||||||
|
seed = seed.strip()
|
||||||
|
if seed:
|
||||||
|
try:
|
||||||
|
seed.decode('hex')
|
||||||
|
return OLD_SEED_VERSION, str(seed)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
words = seed.split()
|
||||||
|
seed = old_mnemonic.mn_decode(words)
|
||||||
|
if not seed:
|
||||||
|
raise Exception("Invalid seed")
|
||||||
|
return OLD_SEED_VERSION, seed
|
||||||
|
|
||||||
|
def get_mnemonic(self, password):
|
||||||
|
import old_mnemonic
|
||||||
|
s = self.get_seed(password)
|
||||||
|
return ' '.join(old_mnemonic.mn_encode(s))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def mpk_from_seed(klass, seed):
|
||||||
|
secexp = klass.stretch_key(seed)
|
||||||
|
master_private_key = ecdsa.SigningKey.from_secret_exponent(secexp, curve = SECP256k1)
|
||||||
|
master_public_key = master_private_key.get_verifying_key().to_string()
|
||||||
|
return master_public_key
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def stretch_key(self, seed):
|
||||||
|
x = seed
|
||||||
|
for i in range(100000):
|
||||||
|
x = hashlib.sha256(x + seed).digest()
|
||||||
|
return string_to_number(x)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_sequence(self, mpk, for_change, n):
|
||||||
|
return string_to_number(Hash("%d:%d:"%(n, for_change) + mpk))
|
||||||
|
|
||||||
|
def get_address(self, for_change, n):
|
||||||
|
pubkey = self.get_pubkey(for_change, n)
|
||||||
|
address = public_key_to_bc_address(pubkey.decode('hex'))
|
||||||
|
return address
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_pubkey_from_mpk(self, mpk, for_change, n):
|
||||||
|
z = self.get_sequence(mpk, for_change, n)
|
||||||
|
master_public_key = ecdsa.VerifyingKey.from_string(mpk, curve = SECP256k1)
|
||||||
|
pubkey_point = master_public_key.pubkey.point + z*SECP256k1.generator
|
||||||
|
public_key2 = ecdsa.VerifyingKey.from_public_point(pubkey_point, curve = SECP256k1)
|
||||||
|
return '04' + public_key2.to_string().encode('hex')
|
||||||
|
|
||||||
|
def derive_pubkey(self, for_change, n):
|
||||||
|
return self.get_pubkey_from_mpk(self.mpk, for_change, n)
|
||||||
|
|
||||||
|
def get_private_key_from_stretched_exponent(self, for_change, n, secexp):
|
||||||
|
order = generator_secp256k1.order()
|
||||||
|
secexp = (secexp + self.get_sequence(self.mpk, for_change, n)) % order
|
||||||
|
pk = number_to_string(secexp, generator_secp256k1.order())
|
||||||
|
compressed = False
|
||||||
|
return SecretToASecret(pk, compressed)
|
||||||
|
|
||||||
|
def get_private_key(self, sequence, password):
|
||||||
|
seed = self.get_seed(password)
|
||||||
|
self.check_seed(seed)
|
||||||
|
for_change, n = sequence
|
||||||
|
secexp = self.stretch_key(seed)
|
||||||
|
pk = self.get_private_key_from_stretched_exponent(for_change, n, secexp)
|
||||||
|
return pk
|
||||||
|
|
||||||
|
def check_seed(self, seed):
|
||||||
|
secexp = self.stretch_key(seed)
|
||||||
|
master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
|
||||||
|
master_public_key = master_private_key.get_verifying_key().to_string()
|
||||||
|
if master_public_key != self.mpk:
|
||||||
|
print_error('invalid password (mpk)', self.mpk.encode('hex'), master_public_key.encode('hex'))
|
||||||
|
raise InvalidPassword()
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
seed = self.get_seed(password)
|
||||||
|
self.check_seed(seed)
|
||||||
|
|
||||||
|
def get_master_public_key(self):
|
||||||
|
return self.mpk.encode('hex')
|
||||||
|
|
||||||
|
def get_xpubkeys(self, for_change, n):
|
||||||
|
s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (for_change, n)))
|
||||||
|
mpk = self.mpk.encode('hex')
|
||||||
|
x_pubkey = 'fe' + mpk + s
|
||||||
|
return [ x_pubkey ]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_xpubkey(self, x_pubkey):
|
||||||
|
assert is_extended_pubkey(x_pubkey)
|
||||||
|
pk = x_pubkey[2:]
|
||||||
|
mpk = pk[0:128]
|
||||||
|
dd = pk[128:]
|
||||||
|
s = []
|
||||||
|
while dd:
|
||||||
|
n = int(bitcoin.rev_hex(dd[0:4]), 16)
|
||||||
|
dd = dd[4:]
|
||||||
|
s.append(n)
|
||||||
|
assert len(s) == 2
|
||||||
|
return mpk, s
|
||||||
|
|
||||||
|
def update_password(self, old_password, new_password):
|
||||||
|
if old_password is not None:
|
||||||
|
self.check_password(old_password)
|
||||||
|
if new_password == '':
|
||||||
|
new_password = None
|
||||||
|
if self.has_seed():
|
||||||
|
decoded = self.get_seed(old_password)
|
||||||
|
self.seed = pw_encode(decoded, new_password)
|
||||||
|
self.use_encryption = (new_password is not None)
|
||||||
|
|
||||||
|
|
||||||
|
class Hardware_KeyStore(KeyStore, Xpub):
|
||||||
|
# Derived classes must set:
|
||||||
|
# - device
|
||||||
|
# - DEVICE_IDS
|
||||||
|
# - wallet_type
|
||||||
|
|
||||||
|
#restore_wallet_class = BIP32_RD_Wallet
|
||||||
|
max_change_outputs = 1
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
Xpub.__init__(self)
|
||||||
|
KeyStore.__init__(self)
|
||||||
|
# Errors and other user interaction is done through the wallet's
|
||||||
|
# handler. The handler is per-window and preserved across
|
||||||
|
# device reconnects
|
||||||
|
self.handler = None
|
||||||
|
|
||||||
|
def is_deterministic(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def load(self, storage, name):
|
||||||
|
self.xpub = storage.get('master_public_keys', {}).get(name)
|
||||||
|
|
||||||
|
def save(self, storage, name):
|
||||||
|
d = storage.get('master_public_keys', {})
|
||||||
|
d[name] = self.xpub
|
||||||
|
storage.put('master_public_keys', d)
|
||||||
|
|
||||||
|
def unpaired(self):
|
||||||
|
'''A device paired with the wallet was diconnected. This can be
|
||||||
|
called in any thread context.'''
|
||||||
|
self.print_error("unpaired")
|
||||||
|
|
||||||
|
def paired(self):
|
||||||
|
'''A device paired with the wallet was (re-)connected. This can be
|
||||||
|
called in any thread context.'''
|
||||||
|
self.print_error("paired")
|
||||||
|
|
||||||
|
def can_export(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_watching_only(self):
|
||||||
|
'''The wallet is not watching-only; the user will be prompted for
|
||||||
|
pin and passphrase as appropriate when needed.'''
|
||||||
|
assert not self.has_seed()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_change_password(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def derive_xkeys(self, root, derivation, password):
|
||||||
|
if self.master_public_keys.get(self.root_name):
|
||||||
|
return BIP44_wallet.derive_xkeys(self, root, derivation, password)
|
||||||
|
# When creating a wallet we need to ask the device for the
|
||||||
|
# master public key
|
||||||
|
xpub = self.get_public_key(derivation)
|
||||||
|
return xpub, None
|
||||||
|
|
||||||
|
|
||||||
|
class BIP44_KeyStore(BIP32_KeyStore):
|
||||||
|
root_derivation = "m/44'/0'/0'"
|
||||||
|
|
||||||
|
def normalize_passphrase(self, passphrase):
|
||||||
|
return normalize('NFKD', unicode(passphrase or ''))
|
||||||
|
|
||||||
|
def is_valid_seed(self, seed):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def mnemonic_to_seed(self, mnemonic, passphrase):
|
||||||
|
# See BIP39
|
||||||
|
import pbkdf2, hashlib, hmac
|
||||||
|
PBKDF2_ROUNDS = 2048
|
||||||
|
mnemonic = normalize('NFKD', ' '.join(mnemonic.split()))
|
||||||
|
passphrase = self.normalize_passphrase(passphrase)
|
||||||
|
return pbkdf2.PBKDF2(mnemonic, 'mnemonic' + passphrase,
|
||||||
|
iterations = PBKDF2_ROUNDS, macmodule = hmac,
|
||||||
|
digestmodule = hashlib.sha512).read(64)
|
||||||
|
|
||||||
|
def on_restore_wallet(self, wizard):
|
||||||
|
#assert isinstance(keystore, self.keystore_class)
|
||||||
|
#msg = _("Enter the seed for your %s wallet:" % self.device)
|
||||||
|
#title=_('Restore hardware wallet'),
|
||||||
|
f = lambda seed: wizard.run('on_restore_seed', seed)
|
||||||
|
wizard.restore_seed_dialog(run_next=f, is_valid=self.is_valid_seed)
|
||||||
|
|
||||||
|
def on_restore_seed(self, wizard, seed):
|
||||||
|
f = lambda passphrase: wizard.run('on_restore_passphrase', seed, passphrase)
|
||||||
|
self.device = ''
|
||||||
|
wizard.request_passphrase(self.device, run_next=f)
|
||||||
|
|
||||||
|
def on_restore_passphrase(self, wizard, seed, passphrase):
|
||||||
|
f = lambda pw: wizard.run('on_restore_password', seed, passphrase, pw)
|
||||||
|
wizard.request_password(run_next=f)
|
||||||
|
|
||||||
|
def on_restore_password(self, wizard, seed, passphrase, password):
|
||||||
|
self.add_seed_and_xprv(seed, password, passphrase)
|
||||||
|
self.save(wizard.storage, 'x/')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
keystores = []
|
||||||
|
|
||||||
|
def load_keystore(storage, name):
|
||||||
|
w = storage.get('wallet_type')
|
||||||
|
t = storage.get('key_type', 'seed')
|
||||||
|
seed_version = storage.get_seed_version()
|
||||||
|
if seed_version == OLD_SEED_VERSION or w == 'old':
|
||||||
|
k = Old_KeyStore()
|
||||||
|
elif t == 'imported':
|
||||||
|
k = Imported_KeyStore()
|
||||||
|
elif name and name not in [ 'x/', 'x1/' ]:
|
||||||
|
k = BIP32_KeyStore()
|
||||||
|
elif t == 'seed':
|
||||||
|
k = BIP32_KeyStore()
|
||||||
|
elif t == 'hardware':
|
||||||
|
hw_type = storage.get('hardware_type')
|
||||||
|
for cat, _type, constructor in keystores:
|
||||||
|
if cat == 'hardware' and _type == hw_type:
|
||||||
|
k = constructor()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise BaseException('unknown hardware type')
|
||||||
|
elif t == 'hw_seed':
|
||||||
|
k = BIP44_KeyStore()
|
||||||
|
else:
|
||||||
|
raise BaseException('unknown wallet type', t)
|
||||||
|
k.load(storage, name)
|
||||||
|
return k
|
||||||
|
|
||||||
|
|
||||||
|
def register_keystore(category, type, constructor):
|
||||||
|
keystores.append((category, type, constructor))
|
||||||
|
|
||||||
|
|
||||||
|
def is_old_mpk(mpk):
|
||||||
|
try:
|
||||||
|
int(mpk, 16)
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
return len(mpk) == 128
|
||||||
|
|
||||||
|
def is_xpub(text):
|
||||||
|
if text[0:4] != 'xpub':
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
deserialize_xkey(text)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_xprv(text):
|
||||||
|
if text[0:4] != 'xprv':
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
deserialize_xkey(text)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_address_list(text):
|
||||||
|
parts = text.split()
|
||||||
|
return bool(parts) and all(bitcoin.is_address(x) for x in parts)
|
||||||
|
|
||||||
|
def is_private_key_list(text):
|
||||||
|
parts = text.split()
|
||||||
|
return bool(parts) and all(bitcoin.is_private_key(x) for x in parts)
|
||||||
|
|
||||||
|
is_seed = lambda x: is_old_seed(x) or is_new_seed(x)
|
||||||
|
is_mpk = lambda x: is_old_mpk(x) or is_xpub(x)
|
||||||
|
is_private = lambda x: is_seed(x) or is_xprv(x) or is_private_key_list(x)
|
||||||
|
is_any_key = lambda x: is_old_mpk(x) or is_xprv(x) or is_xpub(x) or is_address_list(x) or is_private_key_list(x)
|
||||||
|
is_private_key = lambda x: is_xprv(x) or is_private_key_list(x)
|
||||||
|
is_bip32_key = lambda x: is_xprv(x) or is_xpub(x)
|
||||||
|
|
||||||
|
|
||||||
|
def from_seed(seed, password):
|
||||||
|
if is_old_seed(seed):
|
||||||
|
keystore = Old_KeyStore()
|
||||||
|
keystore.add_seed(seed, password)
|
||||||
|
elif is_new_seed(seed):
|
||||||
|
keystore = BIP32_KeyStore()
|
||||||
|
keystore.add_seed_and_xprv(seed, password)
|
||||||
|
return keystore
|
||||||
|
|
||||||
|
def from_private_key_list(text, password):
|
||||||
|
keystore = Imported_KeyStore()
|
||||||
|
for x in text.split():
|
||||||
|
keystore.import_key(x, None)
|
||||||
|
keystore.update_password(None, password)
|
||||||
|
return keystore
|
||||||
|
|
||||||
|
def from_old_mpk(mpk):
|
||||||
|
keystore = Old_KeyStore()
|
||||||
|
keystore.add_master_public_key(mpk)
|
||||||
|
return keystore
|
||||||
|
|
||||||
|
def from_xpub(xpub):
|
||||||
|
keystore = BIP32_KeyStore()
|
||||||
|
keystore.add_master_public_key(xpub)
|
||||||
|
return keystore
|
||||||
|
|
||||||
|
def from_xprv(xprv, password):
|
||||||
|
xpub = bitcoin.xpub_from_xprv(xprv)
|
||||||
|
keystore = BIP32_KeyStore()
|
||||||
|
keystore.add_master_private_key(xprv, password)
|
||||||
|
keystore.add_master_public_key(xpub)
|
||||||
|
return keystore
|
||||||
|
|
||||||
|
def xprv_from_seed(seed, password):
|
||||||
|
# do not store the seed, only the master xprv
|
||||||
|
xprv, xpub = bip32_root(Mnemonic.mnemonic_to_seed(seed, ''))
|
||||||
|
#xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation)
|
||||||
|
return from_xprv(xprv, password)
|
||||||
|
|
||||||
|
def xpub_from_seed(seed):
|
||||||
|
# store only master xpub
|
||||||
|
xprv, xpub = bip32_root(Mnemonic.mnemonic_to_seed(seed,''))
|
||||||
|
#xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation)
|
||||||
|
return from_xpub(xpub)
|
||||||
|
|
||||||
|
def from_text(text, password):
|
||||||
|
if is_xprv(text):
|
||||||
|
k = from_xprv(text, password)
|
||||||
|
elif is_old_mpk(text):
|
||||||
|
k = from_old_mpk(text)
|
||||||
|
elif is_xpub(text):
|
||||||
|
k = from_xpub(text)
|
||||||
|
elif is_private_key_list(text):
|
||||||
|
k = from_private_key_list(text, password)
|
||||||
|
elif is_seed(text):
|
||||||
|
k = from_seed(text, password)
|
||||||
|
else:
|
||||||
|
raise BaseException('Invalid seedphrase or key')
|
||||||
|
return k
|
|
@ -35,6 +35,10 @@ from util import *
|
||||||
from i18n import _
|
from i18n import _
|
||||||
from util import profiler, PrintError, DaemonThread, UserCancelled
|
from util import profiler, PrintError, DaemonThread, UserCancelled
|
||||||
|
|
||||||
|
plugin_loaders = {}
|
||||||
|
hook_names = set()
|
||||||
|
hooks = {}
|
||||||
|
|
||||||
|
|
||||||
class Plugins(DaemonThread):
|
class Plugins(DaemonThread):
|
||||||
|
|
||||||
|
@ -66,15 +70,17 @@ class Plugins(DaemonThread):
|
||||||
continue
|
continue
|
||||||
details = d.get('registers_wallet_type')
|
details = d.get('registers_wallet_type')
|
||||||
if details:
|
if details:
|
||||||
self.register_plugin_wallet(name, gui_good, details)
|
self.register_wallet_type(name, gui_good, details)
|
||||||
|
details = d.get('registers_keystore')
|
||||||
|
if details:
|
||||||
|
self.register_keystore(name, gui_good, details)
|
||||||
self.descriptions[name] = d
|
self.descriptions[name] = d
|
||||||
if not d.get('requires_wallet_type') and self.config.get('use_' + name):
|
if not d.get('requires_wallet_type') and self.config.get('use_' + name):
|
||||||
try:
|
try:
|
||||||
self.load_plugin(name)
|
self.load_plugin(name)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
traceback.print_exc(file=sys.stdout)
|
traceback.print_exc(file=sys.stdout)
|
||||||
self.print_error("cannot initialize plugin %s:" % name,
|
self.print_error("cannot initialize plugin %s:" % name, str(e))
|
||||||
str(e))
|
|
||||||
|
|
||||||
def get(self, name):
|
def get(self, name):
|
||||||
return self.plugins.get(name)
|
return self.plugins.get(name)
|
||||||
|
@ -83,6 +89,8 @@ class Plugins(DaemonThread):
|
||||||
return len(self.plugins)
|
return len(self.plugins)
|
||||||
|
|
||||||
def load_plugin(self, name):
|
def load_plugin(self, name):
|
||||||
|
if name in self.plugins:
|
||||||
|
return
|
||||||
full_name = 'electrum_plugins.' + name + '.' + self.gui_name
|
full_name = 'electrum_plugins.' + name + '.' + self.gui_name
|
||||||
loader = pkgutil.find_loader(full_name)
|
loader = pkgutil.find_loader(full_name)
|
||||||
if not loader:
|
if not loader:
|
||||||
|
@ -145,17 +153,23 @@ class Plugins(DaemonThread):
|
||||||
self.print_error("cannot load plugin for:", name)
|
self.print_error("cannot load plugin for:", name)
|
||||||
return wallet_types, descs
|
return wallet_types, descs
|
||||||
|
|
||||||
def register_plugin_wallet(self, name, gui_good, details):
|
def register_wallet_type(self, name, gui_good, details):
|
||||||
from wallet import Wallet
|
from wallet import Wallet
|
||||||
|
global plugin_loaders
|
||||||
|
def 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):
|
def register_keystore(self, name, gui_good, details):
|
||||||
return self.wallet_plugin_loader(name).wallet_class(storage)
|
from keystore import register_keystore
|
||||||
|
def dynamic_constructor():
|
||||||
|
return self.wallet_plugin_loader(name).keystore_class()
|
||||||
if details[0] == 'hardware':
|
if details[0] == 'hardware':
|
||||||
self.hw_wallets[name] = (gui_good, details)
|
self.hw_wallets[name] = (gui_good, details)
|
||||||
self.print_error("registering wallet %s: %s" %(name, details))
|
self.print_error("registering keystore %s: %s" %(name, details))
|
||||||
Wallet.register_plugin_wallet(details[0], details[1],
|
register_keystore(details[0], details[1], dynamic_constructor)
|
||||||
dynamic_constructor)
|
|
||||||
|
|
||||||
def wallet_plugin_loader(self, name):
|
def wallet_plugin_loader(self, name):
|
||||||
if not name in self.plugins:
|
if not name in self.plugins:
|
||||||
|
@ -169,9 +183,6 @@ class Plugins(DaemonThread):
|
||||||
self.on_stop()
|
self.on_stop()
|
||||||
|
|
||||||
|
|
||||||
hook_names = set()
|
|
||||||
hooks = {}
|
|
||||||
|
|
||||||
def hook(func):
|
def hook(func):
|
||||||
hook_names.add(func.func_name)
|
hook_names.add(func.func_name)
|
||||||
return func
|
return func
|
||||||
|
@ -375,48 +386,45 @@ class DeviceMgr(ThreadJob, PrintError):
|
||||||
self.scan_devices(handler)
|
self.scan_devices(handler)
|
||||||
return self.client_lookup(id_)
|
return self.client_lookup(id_)
|
||||||
|
|
||||||
def client_for_wallet(self, plugin, wallet, force_pair):
|
def client_for_keystore(self, plugin, keystore, force_pair):
|
||||||
assert wallet.handler
|
assert keystore.handler
|
||||||
|
devices = self.scan_devices(keystore.handler)
|
||||||
devices = self.scan_devices(wallet.handler)
|
wallet_id = self.wallet_id(keystore)
|
||||||
wallet_id = self.wallet_id(wallet)
|
|
||||||
|
|
||||||
client = self.client_lookup(wallet_id)
|
client = self.client_lookup(wallet_id)
|
||||||
if client:
|
if client:
|
||||||
# An unpaired client might have another wallet's handler
|
# An unpaired client might have another wallet's handler
|
||||||
# from a prior scan. Replace to fix dialog parenting.
|
# from a prior scan. Replace to fix dialog parenting.
|
||||||
client.handler = wallet.handler
|
client.handler = keystore.handler
|
||||||
return client
|
return client
|
||||||
|
|
||||||
for device in devices:
|
for device in devices:
|
||||||
if device.id_ == wallet_id:
|
if device.id_ == wallet_id:
|
||||||
return self.create_client(device, wallet.handler, plugin)
|
return self.create_client(device, keystore.handler, plugin)
|
||||||
|
|
||||||
if force_pair:
|
if force_pair:
|
||||||
return self.force_pair_wallet(plugin, wallet, devices)
|
return self.force_pair_wallet(plugin, keystore, devices)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def force_pair_wallet(self, plugin, wallet, devices):
|
def force_pair_wallet(self, plugin, keystore, devices):
|
||||||
first_address, derivation = wallet.first_address()
|
xpub = keystore.get_master_public_key()
|
||||||
assert first_address
|
derivation = keystore.get_derivation()
|
||||||
|
|
||||||
# The wallet has not been previously paired, so let the user
|
# The wallet has not been previously paired, so let the user
|
||||||
# choose an unpaired device and compare its first address.
|
# choose an unpaired device and compare its first address.
|
||||||
info = self.select_device(wallet, plugin, devices)
|
info = self.select_device(keystore, plugin, devices)
|
||||||
|
|
||||||
client = self.client_lookup(info.device.id_)
|
client = self.client_lookup(info.device.id_)
|
||||||
if client and client.is_pairable():
|
if client and client.is_pairable():
|
||||||
# See comment above for same code
|
# See comment above for same code
|
||||||
client.handler = wallet.handler
|
client.handler = keystore.handler
|
||||||
# This will trigger a PIN/passphrase entry request
|
# This will trigger a PIN/passphrase entry request
|
||||||
try:
|
try:
|
||||||
client_first_address = client.first_address(derivation)
|
client_xpub = client.get_xpub(derivation)
|
||||||
except (UserCancelled, RuntimeError):
|
except (UserCancelled, RuntimeError):
|
||||||
# Bad / cancelled PIN / passphrase
|
# Bad / cancelled PIN / passphrase
|
||||||
client_first_address = None
|
client_xpub = None
|
||||||
if client_first_address == first_address:
|
if client_xpub == xpub:
|
||||||
self.pair_wallet(wallet, info.device.id_)
|
self.pair_wallet(keystore, info.device.id_)
|
||||||
return client
|
return client
|
||||||
|
|
||||||
# The user input has wrong PIN or passphrase, or cancelled input,
|
# The user input has wrong PIN or passphrase, or cancelled input,
|
||||||
|
|
253
lib/storage.py
Normal file
253
lib/storage.py
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# Electrum - lightweight Bitcoin client
|
||||||
|
# Copyright (C) 2015 Thomas Voegtlin
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person
|
||||||
|
# obtaining a copy of this software and associated documentation files
|
||||||
|
# (the "Software"), to deal in the Software without restriction,
|
||||||
|
# including without limitation the rights to use, copy, modify, merge,
|
||||||
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
|
# and to permit persons to whom the Software is furnished to do so,
|
||||||
|
# subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import ast
|
||||||
|
import threading
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import copy
|
||||||
|
import re
|
||||||
|
import stat
|
||||||
|
|
||||||
|
from i18n import _
|
||||||
|
from util import NotEnoughFunds, PrintError, profiler
|
||||||
|
from plugins import run_hook, plugin_loaders
|
||||||
|
|
||||||
|
class WalletStorage(PrintError):
|
||||||
|
|
||||||
|
def __init__(self, path):
|
||||||
|
self.lock = threading.RLock()
|
||||||
|
self.data = {}
|
||||||
|
self.path = path
|
||||||
|
self.file_exists = False
|
||||||
|
self.modified = False
|
||||||
|
self.print_error("wallet path", self.path)
|
||||||
|
if self.path:
|
||||||
|
self.read(self.path)
|
||||||
|
|
||||||
|
# check here if I need to load a plugin
|
||||||
|
t = self.get('wallet_type')
|
||||||
|
l = plugin_loaders.get(t)
|
||||||
|
if l: l()
|
||||||
|
|
||||||
|
|
||||||
|
def read(self, path):
|
||||||
|
"""Read the contents of the wallet file."""
|
||||||
|
try:
|
||||||
|
with open(self.path, "r") as f:
|
||||||
|
data = f.read()
|
||||||
|
except IOError:
|
||||||
|
return
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.data = json.loads(data)
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
d = ast.literal_eval(data) #parse raw data from reading wallet file
|
||||||
|
labels = d.get('labels', {})
|
||||||
|
except Exception as e:
|
||||||
|
raise IOError("Cannot read wallet file '%s'" % self.path)
|
||||||
|
self.data = {}
|
||||||
|
# In old versions of Electrum labels were latin1 encoded, this fixes breakage.
|
||||||
|
for i, label in labels.items():
|
||||||
|
try:
|
||||||
|
unicode(label)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
d['labels'][i] = unicode(label.decode('latin1'))
|
||||||
|
for key, value in d.items():
|
||||||
|
try:
|
||||||
|
json.dumps(key)
|
||||||
|
json.dumps(value)
|
||||||
|
except:
|
||||||
|
self.print_error('Failed to convert label to json format', key)
|
||||||
|
continue
|
||||||
|
self.data[key] = value
|
||||||
|
self.file_exists = True
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
with self.lock:
|
||||||
|
v = self.data.get(key)
|
||||||
|
if v is None:
|
||||||
|
v = default
|
||||||
|
else:
|
||||||
|
v = copy.deepcopy(v)
|
||||||
|
return v
|
||||||
|
|
||||||
|
def put(self, key, value):
|
||||||
|
try:
|
||||||
|
json.dumps(key)
|
||||||
|
json.dumps(value)
|
||||||
|
except:
|
||||||
|
self.print_error("json error: cannot save", key)
|
||||||
|
return
|
||||||
|
with self.lock:
|
||||||
|
if value is not None:
|
||||||
|
if self.data.get(key) != value:
|
||||||
|
self.modified = True
|
||||||
|
self.data[key] = copy.deepcopy(value)
|
||||||
|
elif key in self.data:
|
||||||
|
self.modified = True
|
||||||
|
self.data.pop(key)
|
||||||
|
|
||||||
|
def write(self):
|
||||||
|
with self.lock:
|
||||||
|
self._write()
|
||||||
|
self.file_exists = True
|
||||||
|
|
||||||
|
def _write(self):
|
||||||
|
if threading.currentThread().isDaemon():
|
||||||
|
self.print_error('warning: daemon thread cannot write wallet')
|
||||||
|
return
|
||||||
|
if not self.modified:
|
||||||
|
return
|
||||||
|
s = json.dumps(self.data, indent=4, sort_keys=True)
|
||||||
|
temp_path = "%s.tmp.%s" % (self.path, os.getpid())
|
||||||
|
with open(temp_path, "w") as f:
|
||||||
|
f.write(s)
|
||||||
|
f.flush()
|
||||||
|
os.fsync(f.fileno())
|
||||||
|
|
||||||
|
mode = os.stat(self.path).st_mode if os.path.exists(self.path) else stat.S_IREAD | stat.S_IWRITE
|
||||||
|
# perform atomic write on POSIX systems
|
||||||
|
try:
|
||||||
|
os.rename(temp_path, self.path)
|
||||||
|
except:
|
||||||
|
os.remove(self.path)
|
||||||
|
os.rename(temp_path, self.path)
|
||||||
|
os.chmod(self.path, mode)
|
||||||
|
self.print_error("saved", self.path)
|
||||||
|
self.modified = False
|
||||||
|
|
||||||
|
def requires_split(self):
|
||||||
|
d = self.get('accounts', {})
|
||||||
|
return len(d) > 1
|
||||||
|
|
||||||
|
def split_accounts(storage):
|
||||||
|
result = []
|
||||||
|
# backward compatibility with old wallets
|
||||||
|
d = storage.get('accounts', {})
|
||||||
|
if len(d) < 2:
|
||||||
|
return
|
||||||
|
wallet_type = storage.get('wallet_type')
|
||||||
|
if wallet_type == 'old':
|
||||||
|
assert len(d) == 2
|
||||||
|
storage1 = WalletStorage(storage.path + '.deterministic')
|
||||||
|
storage1.data = copy.deepcopy(storage.data)
|
||||||
|
storage1.put('accounts', {'0': d['0']})
|
||||||
|
storage1.write()
|
||||||
|
storage2 = WalletStorage(storage.path + '.imported')
|
||||||
|
storage2.data = copy.deepcopy(storage.data)
|
||||||
|
storage2.put('accounts', {'/x': d['/x']})
|
||||||
|
storage2.put('seed', None)
|
||||||
|
storage2.put('seed_version', None)
|
||||||
|
storage2.put('master_public_key', None)
|
||||||
|
storage2.put('wallet_type', 'imported')
|
||||||
|
storage2.write()
|
||||||
|
storage2.upgrade()
|
||||||
|
result = [storage1.path, storage2.path]
|
||||||
|
elif wallet_type in ['bip44', 'trezor']:
|
||||||
|
mpk = storage.get('master_public_keys')
|
||||||
|
for k in d.keys():
|
||||||
|
i = int(k)
|
||||||
|
x = d[k]
|
||||||
|
if x.get("pending"):
|
||||||
|
continue
|
||||||
|
xpub = mpk["x/%d'"%i]
|
||||||
|
new_path = storage.path + '.' + k
|
||||||
|
storage2 = WalletStorage(new_path)
|
||||||
|
storage2.data = copy.deepcopy(storage.data)
|
||||||
|
storage2.put('wallet_type', 'standard')
|
||||||
|
if wallet_type in ['trezor', 'keepkey']:
|
||||||
|
storage2.put('key_type', 'hardware')
|
||||||
|
storage2.put('hardware_type', wallet_type)
|
||||||
|
storage2.put('accounts', {'0': x})
|
||||||
|
# need to save derivation and xpub too
|
||||||
|
storage2.put('master_public_keys', {'x/': xpub})
|
||||||
|
storage2.put('account_id', k)
|
||||||
|
storage2.write()
|
||||||
|
result.append(new_path)
|
||||||
|
else:
|
||||||
|
raise BaseException("This wallet has multiple accounts and must be split")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def requires_upgrade(storage):
|
||||||
|
# '/x' is the internal ID for imported accounts
|
||||||
|
return bool(storage.get('accounts', {}).get('/x', {}).get('imported',{}))
|
||||||
|
|
||||||
|
def upgrade(storage):
|
||||||
|
d = storage.get('accounts', {}).get('/x', {}).get('imported',{})
|
||||||
|
addresses = []
|
||||||
|
keypairs = {}
|
||||||
|
for addr, v in d.items():
|
||||||
|
pubkey, privkey = v
|
||||||
|
if privkey:
|
||||||
|
keypairs[pubkey] = privkey
|
||||||
|
else:
|
||||||
|
addresses.append(addr)
|
||||||
|
if addresses and keypairs:
|
||||||
|
raise BaseException('mixed addresses and privkeys')
|
||||||
|
elif addresses:
|
||||||
|
storage.put('addresses', addresses)
|
||||||
|
storage.put('accounts', None)
|
||||||
|
elif keypairs:
|
||||||
|
storage.put('wallet_type', 'standard')
|
||||||
|
storage.put('key_type', 'imported')
|
||||||
|
storage.put('keypairs', keypairs)
|
||||||
|
storage.put('accounts', None)
|
||||||
|
else:
|
||||||
|
raise BaseException('no addresses or privkeys')
|
||||||
|
storage.write()
|
||||||
|
|
||||||
|
def get_action(self):
|
||||||
|
action = run_hook('get_action', self)
|
||||||
|
if action:
|
||||||
|
return action
|
||||||
|
if not self.file_exists:
|
||||||
|
return 'new'
|
||||||
|
|
||||||
|
def get_seed_version(self):
|
||||||
|
from version import OLD_SEED_VERSION, NEW_SEED_VERSION
|
||||||
|
seed_version = self.get('seed_version')
|
||||||
|
if not seed_version:
|
||||||
|
seed_version = OLD_SEED_VERSION if len(self.get('master_public_key','')) == 128 else NEW_SEED_VERSION
|
||||||
|
if seed_version not in [OLD_SEED_VERSION, NEW_SEED_VERSION]:
|
||||||
|
msg = "Your wallet has an unsupported seed version."
|
||||||
|
msg += '\n\nWallet file: %s' % os.path.abspath(self.path)
|
||||||
|
if seed_version in [5, 7, 8, 9, 10]:
|
||||||
|
msg += "\n\nTo open this wallet, try 'git checkout seed_v%d'"%seed_version
|
||||||
|
if seed_version == 6:
|
||||||
|
# version 1.9.8 created v6 wallets when an incorrect seed was entered in the restore dialog
|
||||||
|
msg += '\n\nThis file was created because of a bug in version 1.9.8.'
|
||||||
|
if self.get('master_public_keys') is None and self.get('master_private_keys') is None and self.get('imported_keys') is None:
|
||||||
|
# pbkdf2 was not included with the binaries, and wallet creation aborted.
|
||||||
|
msg += "\nIt does not contain any keys, and can safely be removed."
|
||||||
|
else:
|
||||||
|
# creation was complete if electrum was run from source
|
||||||
|
msg += "\nPlease open this file with Electrum 1.9.8, and move your coins to a new wallet."
|
||||||
|
raise BaseException(msg)
|
||||||
|
return seed_version
|
|
@ -180,7 +180,7 @@ class Synchronizer(ThreadJob):
|
||||||
|
|
||||||
if self.requested_tx:
|
if self.requested_tx:
|
||||||
self.print_error("missing tx", self.requested_tx)
|
self.print_error("missing tx", self.requested_tx)
|
||||||
self.subscribe_to_addresses(set(self.wallet.addresses(True)))
|
self.subscribe_to_addresses(set(self.wallet.get_addresses()))
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
'''Called from the network proxy thread main loop.'''
|
'''Called from the network proxy thread main loop.'''
|
||||||
|
|
|
@ -761,23 +761,6 @@ class Transaction:
|
||||||
out.add(i)
|
out.add(i)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def inputs_to_sign(self):
|
|
||||||
out = set()
|
|
||||||
for txin in self.inputs():
|
|
||||||
num_sig = txin.get('num_sig')
|
|
||||||
if num_sig is None:
|
|
||||||
continue
|
|
||||||
x_signatures = txin['signatures']
|
|
||||||
signatures = filter(None, x_signatures)
|
|
||||||
if len(signatures) == num_sig:
|
|
||||||
# input is complete
|
|
||||||
continue
|
|
||||||
for k, x_pubkey in enumerate(txin['x_pubkeys']):
|
|
||||||
if x_signatures[k] is not None:
|
|
||||||
# this pubkey already signed
|
|
||||||
continue
|
|
||||||
out.add(x_pubkey)
|
|
||||||
return out
|
|
||||||
|
|
||||||
def sign(self, keypairs):
|
def sign(self, keypairs):
|
||||||
for i, txin in enumerate(self.inputs()):
|
for i, txin in enumerate(self.inputs()):
|
||||||
|
|
1327
lib/wallet.py
1327
lib/wallet.py
File diff suppressed because it is too large
Load diff
|
@ -126,7 +126,8 @@ class Plugin(BasePlugin):
|
||||||
self.listener = None
|
self.listener = None
|
||||||
self.keys = []
|
self.keys = []
|
||||||
self.cosigner_list = []
|
self.cosigner_list = []
|
||||||
for key, xpub in wallet.master_public_keys.items():
|
for key, keystore in wallet.keystores.items():
|
||||||
|
xpub = keystore.get_master_public_key()
|
||||||
K = bitcoin.deserialize_xkey(xpub)[-1].encode('hex')
|
K = bitcoin.deserialize_xkey(xpub)[-1].encode('hex')
|
||||||
_hash = bitcoin.Hash(K).encode('hex')
|
_hash = bitcoin.Hash(K).encode('hex')
|
||||||
if wallet.master_private_keys.get(key):
|
if wallet.master_private_keys.get(key):
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
from hw_wallet import BIP44_HW_Wallet
|
|
||||||
from plugin import HW_PluginBase
|
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):
|
def __init__(self, parent, config, name):
|
||||||
BasePlugin.__init__(self, parent, config, name)
|
BasePlugin.__init__(self, parent, config, name)
|
||||||
self.device = self.wallet_class.device
|
self.device = self.keystore_class.device
|
||||||
self.wallet_class.plugin = self
|
self.keystore_class.plugin = self
|
||||||
|
|
||||||
def is_enabled(self):
|
def is_enabled(self):
|
||||||
return self.libraries_available
|
return self.libraries_available
|
||||||
|
@ -48,33 +48,6 @@ class HW_PluginBase(BasePlugin):
|
||||||
|
|
||||||
@hook
|
@hook
|
||||||
def close_wallet(self, wallet):
|
def close_wallet(self, wallet):
|
||||||
if isinstance(wallet, self.wallet_class):
|
if isinstance(wallet.get_keystore(), self.keystore_class):
|
||||||
self.device_manager().unpair_wallet(wallet)
|
self.device_manager().unpair_wallet(wallet)
|
||||||
|
|
||||||
def on_restore_wallet(self, wallet, wizard):
|
|
||||||
assert isinstance(wallet, self.wallet_class)
|
|
||||||
msg = _("Enter the seed for your %s wallet:" % self.device)
|
|
||||||
f = lambda x: wizard.run('on_restore_seed', x)
|
|
||||||
wizard.enter_seed_dialog(run_next=f, title=_('Restore hardware wallet'), message=msg, is_valid=self.is_valid_seed)
|
|
||||||
|
|
||||||
def on_restore_seed(self, wallet, wizard, seed):
|
|
||||||
f = lambda x: wizard.run('on_restore_passphrase', seed, x)
|
|
||||||
wizard.request_passphrase(self.device, run_next=f)
|
|
||||||
|
|
||||||
def on_restore_passphrase(self, wallet, wizard, seed, passphrase):
|
|
||||||
f = lambda x: wizard.run('on_restore_password', seed, passphrase, x)
|
|
||||||
wizard.request_password(run_next=f)
|
|
||||||
|
|
||||||
def on_restore_password(self, wallet, wizard, seed, passphrase, password):
|
|
||||||
# Restored wallets are not hardware wallets
|
|
||||||
wallet_class = self.wallet_class.restore_wallet_class
|
|
||||||
wallet.storage.put('wallet_type', wallet_class.wallet_type)
|
|
||||||
wallet = wallet_class(wallet.storage)
|
|
||||||
wallet.add_seed(seed, password)
|
|
||||||
wallet.add_xprv_from_seed(seed, 'x/', password, passphrase)
|
|
||||||
wallet.create_hd_account(password)
|
|
||||||
wizard.create_addresses()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_valid_seed(seed):
|
|
||||||
return True
|
|
||||||
|
|
|
@ -3,6 +3,6 @@ from electrum.i18n import _
|
||||||
fullname = 'KeepKey'
|
fullname = 'KeepKey'
|
||||||
description = _('Provides support for KeepKey hardware wallet')
|
description = _('Provides support for KeepKey hardware wallet')
|
||||||
requires = [('keepkeylib','github.com/keepkey/python-keepkey')]
|
requires = [('keepkeylib','github.com/keepkey/python-keepkey')]
|
||||||
requires_wallet_type = ['keepkey']
|
#requires_wallet_type = ['keepkey']
|
||||||
registers_wallet_type = ('hardware', 'keepkey', _("KeepKey wallet"))
|
registers_keystore = ('hardware', 'keepkey', _("KeepKey wallet"))
|
||||||
available_for = ['qt', 'cmdline']
|
available_for = ['qt', 'cmdline']
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from ..trezor.plugin import TrezorCompatiblePlugin, TrezorCompatibleWallet
|
from ..trezor.plugin import TrezorCompatiblePlugin, TrezorCompatibleKeyStore
|
||||||
|
|
||||||
|
|
||||||
class KeepKeyWallet(TrezorCompatibleWallet):
|
class KeepKey_KeyStore(TrezorCompatibleKeyStore):
|
||||||
wallet_type = 'keepkey'
|
wallet_type = 'keepkey'
|
||||||
device = 'KeepKey'
|
device = 'KeepKey'
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ class KeepKeyPlugin(TrezorCompatiblePlugin):
|
||||||
firmware_URL = 'https://www.keepkey.com'
|
firmware_URL = 'https://www.keepkey.com'
|
||||||
libraries_URL = 'https://github.com/keepkey/python-keepkey'
|
libraries_URL = 'https://github.com/keepkey/python-keepkey'
|
||||||
minimum_firmware = (1, 0, 0)
|
minimum_firmware = (1, 0, 0)
|
||||||
wallet_class = KeepKeyWallet
|
keystore_class = KeepKey_KeyStore
|
||||||
try:
|
try:
|
||||||
from .client import KeepKeyClient as client_class
|
from .client import KeepKeyClient as client_class
|
||||||
import keepkeylib.ckd_public as ckd_public
|
import keepkeylib.ckd_public as ckd_public
|
||||||
|
|
|
@ -3,6 +3,6 @@ from electrum.i18n import _
|
||||||
fullname = 'Ledger Wallet'
|
fullname = 'Ledger Wallet'
|
||||||
description = 'Provides support for Ledger hardware wallet'
|
description = 'Provides support for Ledger hardware wallet'
|
||||||
requires = [('btchip', 'github.com/ledgerhq/btchip-python')]
|
requires = [('btchip', 'github.com/ledgerhq/btchip-python')]
|
||||||
requires_wallet_type = ['btchip']
|
#requires_wallet_type = ['btchip']
|
||||||
registers_wallet_type = ('hardware', 'btchip', _("Ledger wallet"))
|
registers_keystore = ('hardware', 'btchip', _("Ledger wallet"))
|
||||||
available_for = ['qt', 'cmdline']
|
available_for = ['qt', 'cmdline']
|
||||||
|
|
|
@ -7,7 +7,7 @@ import electrum
|
||||||
from electrum.bitcoin import EncodeBase58Check, DecodeBase58Check, TYPE_ADDRESS
|
from electrum.bitcoin import EncodeBase58Check, DecodeBase58Check, TYPE_ADDRESS
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.plugins import BasePlugin, hook
|
from electrum.plugins import BasePlugin, hook
|
||||||
from ..hw_wallet import BIP44_HW_Wallet
|
from ..hw_wallet import BIP32_HW_Wallet
|
||||||
from ..hw_wallet import HW_PluginBase
|
from ..hw_wallet import HW_PluginBase
|
||||||
from electrum.util import format_satoshis_plain, print_error
|
from electrum.util import format_satoshis_plain, print_error
|
||||||
|
|
||||||
|
@ -26,12 +26,12 @@ except ImportError:
|
||||||
BTCHIP = False
|
BTCHIP = False
|
||||||
|
|
||||||
|
|
||||||
class BTChipWallet(BIP44_HW_Wallet):
|
class BTChipWallet(BIP32_HW_Wallet):
|
||||||
wallet_type = 'btchip'
|
wallet_type = 'btchip'
|
||||||
device = 'Ledger'
|
device = 'Ledger'
|
||||||
|
|
||||||
def __init__(self, storage):
|
def __init__(self, storage):
|
||||||
BIP44_HW_Wallet.__init__(self, storage)
|
BIP32_HW_Wallet.__init__(self, storage)
|
||||||
# Errors and other user interaction is done through the wallet's
|
# Errors and other user interaction is done through the wallet's
|
||||||
# handler. The handler is per-window and preserved across
|
# handler. The handler is per-window and preserved across
|
||||||
# device reconnects
|
# device reconnects
|
||||||
|
@ -53,7 +53,7 @@ class BTChipWallet(BIP44_HW_Wallet):
|
||||||
|
|
||||||
def address_id(self, address):
|
def address_id(self, address):
|
||||||
# Strip the leading "m/"
|
# Strip the leading "m/"
|
||||||
return BIP44_HW_Wallet.address_id(self, address)[2:]
|
return BIP32_HW_Wallet.address_id(self, address)[2:]
|
||||||
|
|
||||||
def get_public_key(self, bip32_path):
|
def get_public_key(self, bip32_path):
|
||||||
# bip32_path is of the form 44'/0'/1'
|
# bip32_path is of the form 44'/0'/1'
|
||||||
|
|
|
@ -3,7 +3,7 @@ from electrum.i18n import _
|
||||||
fullname = 'TREZOR Wallet'
|
fullname = 'TREZOR Wallet'
|
||||||
description = _('Provides support for TREZOR hardware wallet')
|
description = _('Provides support for TREZOR hardware wallet')
|
||||||
requires = [('trezorlib','github.com/trezor/python-trezor')]
|
requires = [('trezorlib','github.com/trezor/python-trezor')]
|
||||||
requires_wallet_type = ['trezor']
|
#requires_wallet_type = ['trezor']
|
||||||
registers_wallet_type = ('hardware', 'trezor', _("TREZOR wallet"))
|
registers_keystore = ('hardware', 'trezor', _("TREZOR wallet"))
|
||||||
available_for = ['qt', 'cmdline']
|
available_for = ['qt', 'cmdline']
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import time
|
import time
|
||||||
|
from struct import pack
|
||||||
|
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.util import PrintError, UserCancelled
|
from electrum.util import PrintError, UserCancelled
|
||||||
from electrum.wallet import BIP44_Wallet
|
from electrum.keystore import BIP44_KeyStore
|
||||||
|
from electrum.bitcoin import EncodeBase58Check
|
||||||
|
|
||||||
|
|
||||||
class GuiMixin(object):
|
class GuiMixin(object):
|
||||||
|
@ -63,7 +65,7 @@ class GuiMixin(object):
|
||||||
passphrase = self.handler.get_passphrase(msg, self.creating_wallet)
|
passphrase = self.handler.get_passphrase(msg, self.creating_wallet)
|
||||||
if passphrase is None:
|
if passphrase is None:
|
||||||
return self.proto.Cancel()
|
return self.proto.Cancel()
|
||||||
passphrase = BIP44_Wallet.normalize_passphrase(passphrase)
|
passphrase = BIP44_KeyStore.normalize_passphrase(passphrase)
|
||||||
return self.proto.PassphraseAck(passphrase=passphrase)
|
return self.proto.PassphraseAck(passphrase=passphrase)
|
||||||
|
|
||||||
def callback_WordRequest(self, msg):
|
def callback_WordRequest(self, msg):
|
||||||
|
@ -142,11 +144,20 @@ class TrezorClientBase(GuiMixin, PrintError):
|
||||||
'''Provided here as in keepkeylib but not trezorlib.'''
|
'''Provided here as in keepkeylib but not trezorlib.'''
|
||||||
self.transport.write(self.proto.Cancel())
|
self.transport.write(self.proto.Cancel())
|
||||||
|
|
||||||
def first_address(self, derivation):
|
def i4b(self, x):
|
||||||
return self.address_from_derivation(derivation)
|
return pack('>I', x)
|
||||||
|
|
||||||
def address_from_derivation(self, derivation):
|
def get_xpub(self, bip32_path):
|
||||||
return self.get_address('Bitcoin', self.expand_path(derivation))
|
address_n = self.expand_path(bip32_path)
|
||||||
|
creating = False #self.next_account_number() == 0
|
||||||
|
node = self.get_public_node(address_n, creating).node
|
||||||
|
xpub = ("0488B21E".decode('hex') + chr(node.depth)
|
||||||
|
+ self.i4b(node.fingerprint) + self.i4b(node.child_num)
|
||||||
|
+ node.chain_code + node.public_key)
|
||||||
|
return EncodeBase58Check(xpub)
|
||||||
|
|
||||||
|
#def address_from_derivation(self, derivation):
|
||||||
|
# return self.get_address('Bitcoin', self.expand_path(derivation))
|
||||||
|
|
||||||
def toggle_passphrase(self):
|
def toggle_passphrase(self):
|
||||||
if self.features.passphrase_protection:
|
if self.features.passphrase_protection:
|
||||||
|
|
|
@ -8,28 +8,32 @@ from functools import partial
|
||||||
from electrum.account import BIP32_Account
|
from electrum.account import BIP32_Account
|
||||||
from electrum.bitcoin import (bc_address_to_hash_160, xpub_from_pubkey,
|
from electrum.bitcoin import (bc_address_to_hash_160, xpub_from_pubkey,
|
||||||
public_key_to_bc_address, EncodeBase58Check,
|
public_key_to_bc_address, EncodeBase58Check,
|
||||||
TYPE_ADDRESS)
|
TYPE_ADDRESS, TYPE_SCRIPT)
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.plugins import BasePlugin, hook
|
from electrum.plugins import BasePlugin, hook
|
||||||
from electrum.transaction import (deserialize, is_extended_pubkey,
|
from electrum.transaction import (deserialize, is_extended_pubkey,
|
||||||
Transaction, x_to_xpub)
|
Transaction, x_to_xpub)
|
||||||
from ..hw_wallet import BIP44_HW_Wallet, HW_PluginBase
|
from electrum.keystore import Hardware_KeyStore
|
||||||
|
|
||||||
|
from ..hw_wallet import HW_PluginBase
|
||||||
|
|
||||||
|
|
||||||
# TREZOR initialization methods
|
# TREZOR initialization methods
|
||||||
TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)
|
TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)
|
||||||
|
|
||||||
class TrezorCompatibleWallet(BIP44_HW_Wallet):
|
class TrezorCompatibleKeyStore(Hardware_KeyStore):
|
||||||
|
root = "m/44'/0'"
|
||||||
|
account_id = 0
|
||||||
|
|
||||||
def get_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()
|
client = self.get_client()
|
||||||
address_n = client.expand_path(bip32_path)
|
self.xpub = client.get_xpub(self.get_derivation())
|
||||||
creating = self.next_account_number() == 0
|
|
||||||
node = client.get_public_node(address_n, creating).node
|
|
||||||
xpub = ("0488B21E".decode('hex') + chr(node.depth)
|
|
||||||
+ self.i4b(node.fingerprint) + self.i4b(node.child_num)
|
|
||||||
+ node.chain_code + node.public_key)
|
|
||||||
return EncodeBase58Check(xpub)
|
|
||||||
|
|
||||||
def decrypt_message(self, pubkey, message, password):
|
def decrypt_message(self, pubkey, message, password):
|
||||||
raise RuntimeError(_('Electrum and %s encryption and decryption are currently incompatible') % self.device)
|
raise RuntimeError(_('Electrum and %s encryption and decryption are currently incompatible') % self.device)
|
||||||
|
@ -49,17 +53,6 @@ class TrezorCompatibleWallet(BIP44_HW_Wallet):
|
||||||
msg_sig = client.sign_message('Bitcoin', address_n, message)
|
msg_sig = client.sign_message('Bitcoin', address_n, message)
|
||||||
return msg_sig.signature
|
return msg_sig.signature
|
||||||
|
|
||||||
def get_input_tx(self, tx_hash):
|
|
||||||
# First look up an input transaction in the wallet where it
|
|
||||||
# will likely be. If co-signing a transaction it may not have
|
|
||||||
# all the input txs, in which case we ask the network.
|
|
||||||
tx = self.transactions.get(tx_hash)
|
|
||||||
if not tx:
|
|
||||||
request = ('blockchain.transaction.get', [tx_hash])
|
|
||||||
# FIXME: what if offline?
|
|
||||||
tx = Transaction(self.network.synchronous_get(request))
|
|
||||||
return tx
|
|
||||||
|
|
||||||
def sign_transaction(self, tx, password):
|
def sign_transaction(self, tx, password):
|
||||||
if tx.is_complete():
|
if tx.is_complete():
|
||||||
return
|
return
|
||||||
|
@ -69,15 +62,13 @@ class TrezorCompatibleWallet(BIP44_HW_Wallet):
|
||||||
xpub_path = {}
|
xpub_path = {}
|
||||||
for txin in tx.inputs():
|
for txin in tx.inputs():
|
||||||
tx_hash = txin['prevout_hash']
|
tx_hash = txin['prevout_hash']
|
||||||
prev_tx[tx_hash] = self.get_input_tx(tx_hash)
|
prev_tx[tx_hash] = txin['prev_tx']
|
||||||
for x_pubkey in txin['x_pubkeys']:
|
for x_pubkey in txin['x_pubkeys']:
|
||||||
if not is_extended_pubkey(x_pubkey):
|
if not is_extended_pubkey(x_pubkey):
|
||||||
continue
|
continue
|
||||||
xpub = x_to_xpub(x_pubkey)
|
xpub = x_to_xpub(x_pubkey)
|
||||||
for k, v in self.master_public_keys.items():
|
if xpub == self.get_master_public_key():
|
||||||
if v == xpub:
|
xpub_path[xpub] = self.get_derivation()
|
||||||
acc_id = re.match("x/(\d+)'", k).group(1)
|
|
||||||
xpub_path[xpub] = self.account_derivation(acc_id)
|
|
||||||
|
|
||||||
self.plugin.sign_transaction(self, tx, prev_tx, xpub_path)
|
self.plugin.sign_transaction(self, tx, prev_tx, xpub_path)
|
||||||
|
|
||||||
|
@ -149,18 +140,16 @@ class TrezorCompatiblePlugin(HW_PluginBase):
|
||||||
|
|
||||||
return client
|
return client
|
||||||
|
|
||||||
def get_client(self, wallet, force_pair=True):
|
def get_client(self, keystore, force_pair=True):
|
||||||
# All client interaction should not be in the main GUI thread
|
# All client interaction should not be in the main GUI thread
|
||||||
assert self.main_thread != threading.current_thread()
|
assert self.main_thread != threading.current_thread()
|
||||||
|
|
||||||
devmgr = self.device_manager()
|
devmgr = self.device_manager()
|
||||||
client = devmgr.client_for_wallet(self, wallet, force_pair)
|
client = devmgr.client_for_keystore(self, keystore, force_pair)
|
||||||
if client:
|
if client:
|
||||||
client.used()
|
client.used()
|
||||||
|
|
||||||
return client
|
return client
|
||||||
|
|
||||||
def initialize_device(self, wallet):
|
def initialize_device(self, keystore):
|
||||||
# Initialization method
|
# Initialization method
|
||||||
msg = _("Choose how you want to initialize your %s.\n\n"
|
msg = _("Choose how you want to initialize your %s.\n\n"
|
||||||
"The first two methods are secure as no secret information "
|
"The first two methods are secure as no secret information "
|
||||||
|
@ -179,13 +168,13 @@ class TrezorCompatiblePlugin(HW_PluginBase):
|
||||||
_("Upload a master private key")
|
_("Upload a master private key")
|
||||||
]
|
]
|
||||||
|
|
||||||
method = wallet.handler.query_choice(msg, methods)
|
method = keystore.handler.query_choice(msg, methods)
|
||||||
(item, label, pin_protection, passphrase_protection) \
|
(item, label, pin_protection, passphrase_protection) \
|
||||||
= wallet.handler.request_trezor_init_settings(method, self.device)
|
= wallet.handler.request_trezor_init_settings(method, self.device)
|
||||||
|
|
||||||
if method == TIM_RECOVER and self.device == 'TREZOR':
|
if method == TIM_RECOVER and self.device == 'TREZOR':
|
||||||
# Warn user about firmware lameness
|
# Warn user about firmware lameness
|
||||||
wallet.handler.show_error(_(
|
keystore.handler.show_error(_(
|
||||||
"You will be asked to enter 24 words regardless of your "
|
"You will be asked to enter 24 words regardless of your "
|
||||||
"seed's actual length. If you enter a word incorrectly or "
|
"seed's actual length. If you enter a word incorrectly or "
|
||||||
"misspell it, you cannot change it or go back - you will need "
|
"misspell it, you cannot change it or go back - you will need "
|
||||||
|
@ -195,7 +184,7 @@ class TrezorCompatiblePlugin(HW_PluginBase):
|
||||||
language = 'english'
|
language = 'english'
|
||||||
|
|
||||||
def initialize_method():
|
def initialize_method():
|
||||||
client = self.get_client(wallet)
|
client = self.get_client(keystore)
|
||||||
|
|
||||||
if method == TIM_NEW:
|
if method == TIM_NEW:
|
||||||
strength = 64 * (item + 2) # 128, 192 or 256
|
strength = 64 * (item + 2) # 128, 192 or 256
|
||||||
|
@ -216,35 +205,36 @@ class TrezorCompatiblePlugin(HW_PluginBase):
|
||||||
client.load_device_by_xprv(item, pin, passphrase_protection,
|
client.load_device_by_xprv(item, pin, passphrase_protection,
|
||||||
label, language)
|
label, language)
|
||||||
# After successful initialization create accounts
|
# After successful initialization create accounts
|
||||||
wallet.create_hd_account(None)
|
keystore.init_xpub()
|
||||||
|
#wallet.create_main_account()
|
||||||
|
|
||||||
return initialize_method
|
return initialize_method
|
||||||
|
|
||||||
def setup_device(self, wallet, on_done, on_error):
|
def setup_device(self, keystore, on_done, on_error):
|
||||||
'''Called when creating a new wallet. Select the device to use. If
|
'''Called when creating a new wallet. Select the device to use. If
|
||||||
the device is uninitialized, go through the intialization
|
the device is uninitialized, go through the intialization
|
||||||
process. Then create the wallet accounts.'''
|
process. Then create the wallet accounts.'''
|
||||||
devmgr = self.device_manager()
|
devmgr = self.device_manager()
|
||||||
device_info = devmgr.select_device(wallet, self)
|
device_info = devmgr.select_device(keystore, self)
|
||||||
devmgr.pair_wallet(wallet, device_info.device.id_)
|
devmgr.pair_wallet(keystore, device_info.device.id_)
|
||||||
if device_info.initialized:
|
if device_info.initialized:
|
||||||
task = partial(wallet.create_hd_account, None)
|
task = keystore.init_xpub
|
||||||
else:
|
else:
|
||||||
task = self.initialize_device(wallet)
|
task = self.initialize_device(keystore)
|
||||||
wallet.thread.add(task, on_done=on_done, on_error=on_error)
|
keystore.thread.add(task, on_done=on_done, on_error=on_error)
|
||||||
|
|
||||||
def sign_transaction(self, wallet, tx, prev_tx, xpub_path):
|
def sign_transaction(self, keystore, tx, prev_tx, xpub_path):
|
||||||
self.prev_tx = prev_tx
|
self.prev_tx = prev_tx
|
||||||
self.xpub_path = xpub_path
|
self.xpub_path = xpub_path
|
||||||
client = self.get_client(wallet)
|
client = self.get_client(keystore)
|
||||||
inputs = self.tx_inputs(tx, True)
|
inputs = self.tx_inputs(tx, True)
|
||||||
outputs = self.tx_outputs(wallet, tx)
|
outputs = self.tx_outputs(keystore.get_derivation(), tx)
|
||||||
signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1]
|
signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1]
|
||||||
raw = signed_tx.encode('hex')
|
raw = signed_tx.encode('hex')
|
||||||
tx.update_signatures(raw)
|
tx.update_signatures(raw)
|
||||||
|
|
||||||
def show_address(self, wallet, address):
|
def show_address(self, wallet, address):
|
||||||
client = self.get_client(wallet)
|
client = self.get_client(wallet.keystore)
|
||||||
if not client.atleast_version(1, 3):
|
if not client.atleast_version(1, 3):
|
||||||
wallet.handler.show_error(_("Your device firmware is too old"))
|
wallet.handler.show_error(_("Your device firmware is too old"))
|
||||||
return
|
return
|
||||||
|
@ -313,18 +303,22 @@ class TrezorCompatiblePlugin(HW_PluginBase):
|
||||||
|
|
||||||
return inputs
|
return inputs
|
||||||
|
|
||||||
def tx_outputs(self, wallet, tx):
|
def tx_outputs(self, derivation, tx):
|
||||||
outputs = []
|
outputs = []
|
||||||
for type, address, amount in tx.outputs():
|
for i, (_type, address, amount) in enumerate(tx.outputs()):
|
||||||
assert type == TYPE_ADDRESS
|
|
||||||
txoutputtype = self.types.TxOutputType()
|
txoutputtype = self.types.TxOutputType()
|
||||||
if wallet.is_change(address):
|
txoutputtype.amount = amount
|
||||||
address_path = wallet.address_id(address)
|
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)
|
address_n = self.client_class.expand_path(address_path)
|
||||||
txoutputtype.address_n.extend(address_n)
|
txoutputtype.address_n.extend(address_n)
|
||||||
else:
|
else:
|
||||||
txoutputtype.address = address
|
txoutputtype.address = address
|
||||||
txoutputtype.amount = amount
|
|
||||||
addrtype, hash_160 = bc_address_to_hash_160(address)
|
addrtype, hash_160 = bc_address_to_hash_160(address)
|
||||||
if addrtype == 0:
|
if addrtype == 0:
|
||||||
txoutputtype.script_type = self.types.PAYTOADDRESS
|
txoutputtype.script_type = self.types.PAYTOADDRESS
|
||||||
|
@ -332,6 +326,8 @@ class TrezorCompatiblePlugin(HW_PluginBase):
|
||||||
txoutputtype.script_type = self.types.PAYTOSCRIPTHASH
|
txoutputtype.script_type = self.types.PAYTOSCRIPTHASH
|
||||||
else:
|
else:
|
||||||
raise BaseException('addrtype')
|
raise BaseException('addrtype')
|
||||||
|
else:
|
||||||
|
raise BaseException('addrtype')
|
||||||
outputs.append(txoutputtype)
|
outputs.append(txoutputtype)
|
||||||
|
|
||||||
return outputs
|
return outputs
|
||||||
|
|
|
@ -12,7 +12,7 @@ from ..hw_wallet.qt import QtHandlerBase
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.plugins import hook, DeviceMgr
|
from electrum.plugins import hook, DeviceMgr
|
||||||
from electrum.util import PrintError, UserCancelled
|
from electrum.util import PrintError, UserCancelled
|
||||||
from electrum.wallet import Wallet, BIP44_Wallet
|
from electrum.wallet import Wallet
|
||||||
|
|
||||||
PASSPHRASE_HELP_SHORT =_(
|
PASSPHRASE_HELP_SHORT =_(
|
||||||
"Passphrases allow you to access new wallets, each "
|
"Passphrases allow you to access new wallets, each "
|
||||||
|
@ -273,23 +273,25 @@ def qt_plugin_class(base_plugin_class):
|
||||||
|
|
||||||
@hook
|
@hook
|
||||||
def load_wallet(self, wallet, window):
|
def load_wallet(self, wallet, window):
|
||||||
if type(wallet) != self.wallet_class:
|
keystore = wallet.get_keystore()
|
||||||
|
if type(keystore) != self.keystore_class:
|
||||||
return
|
return
|
||||||
window.tzb = StatusBarButton(QIcon(self.icon_file), self.device,
|
window.tzb = StatusBarButton(QIcon(self.icon_file), self.device,
|
||||||
partial(self.settings_dialog, window))
|
partial(self.settings_dialog, window))
|
||||||
window.statusBar().addPermanentWidget(window.tzb)
|
window.statusBar().addPermanentWidget(window.tzb)
|
||||||
wallet.handler = self.create_handler(window)
|
keystore.handler = self.create_handler(window)
|
||||||
|
keystore.thread = TaskThread(window, window.on_error)
|
||||||
# Trigger a pairing
|
# Trigger a pairing
|
||||||
wallet.thread.add(partial(self.get_client, wallet))
|
keystore.thread.add(partial(self.get_client, keystore))
|
||||||
|
|
||||||
def on_create_wallet(self, wallet, wizard):
|
def on_create_wallet(self, keystore, wizard):
|
||||||
assert type(wallet) == self.wallet_class
|
#assert type(keystore) == self.keystore_class
|
||||||
wallet.handler = self.create_handler(wizard)
|
keystore.handler = self.create_handler(wizard)
|
||||||
wallet.thread = TaskThread(wizard, wizard.on_error)
|
keystore.thread = TaskThread(wizard, wizard.on_error)
|
||||||
# Setup device and create accounts in separate thread; wait until done
|
# Setup device and create accounts in separate thread; wait until done
|
||||||
loop = QEventLoop()
|
loop = QEventLoop()
|
||||||
exc_info = []
|
exc_info = []
|
||||||
self.setup_device(wallet, on_done=loop.quit,
|
self.setup_device(keystore, on_done=loop.quit,
|
||||||
on_error=lambda info: exc_info.extend(info))
|
on_error=lambda info: exc_info.extend(info))
|
||||||
loop.exec_()
|
loop.exec_()
|
||||||
# If an exception was thrown, show to user and exit install wizard
|
# If an exception was thrown, show to user and exit install wizard
|
||||||
|
@ -299,9 +301,10 @@ def qt_plugin_class(base_plugin_class):
|
||||||
|
|
||||||
@hook
|
@hook
|
||||||
def receive_menu(self, menu, addrs, wallet):
|
def receive_menu(self, menu, addrs, wallet):
|
||||||
if type(wallet) == self.wallet_class and len(addrs) == 1:
|
keystore = wallet.get_keystore()
|
||||||
|
if type(keystore) == self.keystore_class and len(addrs) == 1:
|
||||||
def show_address():
|
def show_address():
|
||||||
wallet.thread.add(partial(self.show_address, wallet, addrs[0]))
|
keystore.thread.add(partial(self.show_address, wallet, addrs[0]))
|
||||||
menu.addAction(_("Show on %s") % self.device, show_address)
|
menu.addAction(_("Show on %s") % self.device, show_address)
|
||||||
|
|
||||||
def settings_dialog(self, window):
|
def settings_dialog(self, window):
|
||||||
|
@ -312,9 +315,10 @@ def qt_plugin_class(base_plugin_class):
|
||||||
def choose_device(self, window):
|
def choose_device(self, window):
|
||||||
'''This dialog box should be usable even if the user has
|
'''This dialog box should be usable even if the user has
|
||||||
forgotten their PIN or it is in bootloader mode.'''
|
forgotten their PIN or it is in bootloader mode.'''
|
||||||
device_id = self.device_manager().wallet_id(window.wallet)
|
keystore = window.wallet.get_keystore()
|
||||||
|
device_id = self.device_manager().wallet_id(keystore)
|
||||||
if not device_id:
|
if not device_id:
|
||||||
info = self.device_manager().select_device(window.wallet, self)
|
info = self.device_manager().select_device(keystore, self)
|
||||||
device_id = info.device.id_
|
device_id = info.device.id_
|
||||||
return device_id
|
return device_id
|
||||||
|
|
||||||
|
@ -345,8 +349,9 @@ class SettingsDialog(WindowModalDialog):
|
||||||
|
|
||||||
devmgr = plugin.device_manager()
|
devmgr = plugin.device_manager()
|
||||||
config = devmgr.config
|
config = devmgr.config
|
||||||
handler = window.wallet.handler
|
keystore = window.wallet.get_keystore()
|
||||||
thread = window.wallet.thread
|
handler = keystore.handler
|
||||||
|
thread = keystore.thread
|
||||||
# wallet can be None, needn't be window.wallet
|
# wallet can be None, needn't be window.wallet
|
||||||
wallet = devmgr.wallet_by_id(device_id)
|
wallet = devmgr.wallet_by_id(device_id)
|
||||||
hs_rows, hs_cols = (64, 128)
|
hs_rows, hs_cols = (64, 128)
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
from .plugin import TrezorCompatiblePlugin, TrezorCompatibleWallet
|
from .plugin import TrezorCompatiblePlugin, TrezorCompatibleKeyStore
|
||||||
|
|
||||||
|
|
||||||
class TrezorWallet(TrezorCompatibleWallet):
|
class TrezorKeyStore(TrezorCompatibleKeyStore):
|
||||||
wallet_type = 'trezor'
|
wallet_type = 'trezor'
|
||||||
device = 'TREZOR'
|
device = 'TREZOR'
|
||||||
|
|
||||||
|
|
||||||
class TrezorPlugin(TrezorCompatiblePlugin):
|
class TrezorPlugin(TrezorCompatiblePlugin):
|
||||||
firmware_URL = 'https://www.mytrezor.com'
|
firmware_URL = 'https://www.mytrezor.com'
|
||||||
libraries_URL = 'https://github.com/trezor/python-trezor'
|
libraries_URL = 'https://github.com/trezor/python-trezor'
|
||||||
minimum_firmware = (1, 3, 3)
|
minimum_firmware = (1, 3, 3)
|
||||||
wallet_class = TrezorWallet
|
keystore_class = TrezorKeyStore
|
||||||
try:
|
try:
|
||||||
from .client import TrezorClient as client_class
|
from .client import TrezorClient as client_class
|
||||||
import trezorlib.ckd_public as ckd_public
|
import trezorlib.ckd_public as ckd_public
|
||||||
|
|
|
@ -34,10 +34,11 @@ from urllib import quote
|
||||||
|
|
||||||
import electrum
|
import electrum
|
||||||
from electrum import bitcoin
|
from electrum import bitcoin
|
||||||
|
from electrum import keystore
|
||||||
from electrum.bitcoin import *
|
from electrum.bitcoin import *
|
||||||
from electrum.mnemonic import Mnemonic
|
from electrum.mnemonic import Mnemonic
|
||||||
from electrum import version
|
from electrum import version
|
||||||
from electrum.wallet import Multisig_Wallet, BIP32_Wallet
|
from electrum.wallet import Multisig_Wallet, Deterministic_Wallet, Wallet
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.plugins import BasePlugin, run_hook, hook
|
from electrum.plugins import BasePlugin, run_hook, hook
|
||||||
from electrum.util import NotEnoughFunds
|
from electrum.util import NotEnoughFunds
|
||||||
|
@ -187,29 +188,16 @@ server = TrustedCoinCosignerClient(user_agent="Electrum/" + version.ELECTRUM_VER
|
||||||
class Wallet_2fa(Multisig_Wallet):
|
class Wallet_2fa(Multisig_Wallet):
|
||||||
|
|
||||||
def __init__(self, storage):
|
def __init__(self, storage):
|
||||||
BIP32_Wallet.__init__(self, storage)
|
self.m, self.n = 2, 3
|
||||||
self.wallet_type = '2fa'
|
Deterministic_Wallet.__init__(self, storage)
|
||||||
self.m = 2
|
|
||||||
self.n = 3
|
|
||||||
self.is_billing = False
|
self.is_billing = False
|
||||||
self.billing_info = None
|
self.billing_info = None
|
||||||
|
|
||||||
def get_action(self):
|
|
||||||
xpub1 = self.master_public_keys.get("x1/")
|
|
||||||
xpub2 = self.master_public_keys.get("x2/")
|
|
||||||
xpub3 = self.master_public_keys.get("x3/")
|
|
||||||
if xpub2 is None and not self.storage.get('use_trustedcoin'):
|
|
||||||
return 'show_disclaimer'
|
|
||||||
if xpub2 is None:
|
|
||||||
return 'create_extended_seed'
|
|
||||||
if xpub3 is None:
|
|
||||||
return 'create_remote_key'
|
|
||||||
|
|
||||||
def make_seed(self):
|
|
||||||
return Mnemonic('english').make_seed(num_bits=256, prefix=SEED_PREFIX)
|
|
||||||
|
|
||||||
def can_sign_without_server(self):
|
def can_sign_without_server(self):
|
||||||
return self.master_private_keys.get('x2/') is not None
|
return not self.keystores.get('x2/').is_watching_only()
|
||||||
|
|
||||||
|
def get_user_id(self):
|
||||||
|
return get_user_id(self.storage)
|
||||||
|
|
||||||
def get_max_amount(self, config, inputs, recipient, fee):
|
def get_max_amount(self, config, inputs, recipient, fee):
|
||||||
from electrum.transaction import Transaction
|
from electrum.transaction import Transaction
|
||||||
|
@ -244,7 +232,7 @@ class Wallet_2fa(Multisig_Wallet):
|
||||||
|
|
||||||
def make_unsigned_transaction(self, coins, outputs, config,
|
def make_unsigned_transaction(self, coins, outputs, config,
|
||||||
fixed_fee=None, change_addr=None):
|
fixed_fee=None, change_addr=None):
|
||||||
mk_tx = lambda o: BIP32_Wallet.make_unsigned_transaction(
|
mk_tx = lambda o: Multisig_Wallet.make_unsigned_transaction(
|
||||||
self, coins, o, config, fixed_fee, change_addr)
|
self, coins, o, config, fixed_fee, change_addr)
|
||||||
fee = self.extra_fee()
|
fee = self.extra_fee()
|
||||||
if fee:
|
if fee:
|
||||||
|
@ -264,7 +252,7 @@ class Wallet_2fa(Multisig_Wallet):
|
||||||
return tx
|
return tx
|
||||||
|
|
||||||
def sign_transaction(self, tx, password):
|
def sign_transaction(self, tx, password):
|
||||||
BIP32_Wallet.sign_transaction(self, tx, password)
|
Multisig_Wallet.sign_transaction(self, tx, password)
|
||||||
if tx.is_complete():
|
if tx.is_complete():
|
||||||
return
|
return
|
||||||
if not self.auth_code:
|
if not self.auth_code:
|
||||||
|
@ -279,27 +267,25 @@ class Wallet_2fa(Multisig_Wallet):
|
||||||
tx.update(raw_tx)
|
tx.update(raw_tx)
|
||||||
self.print_error("twofactor: is complete", tx.is_complete())
|
self.print_error("twofactor: is complete", tx.is_complete())
|
||||||
|
|
||||||
def get_user_id(self):
|
|
||||||
def make_long_id(xpub_hot, xpub_cold):
|
|
||||||
return bitcoin.sha256(''.join(sorted([xpub_hot, xpub_cold])))
|
|
||||||
xpub_hot = self.master_public_keys["x1/"]
|
|
||||||
xpub_cold = self.master_public_keys["x2/"]
|
|
||||||
long_id = make_long_id(xpub_hot, xpub_cold)
|
|
||||||
short_id = hashlib.sha256(long_id).hexdigest()
|
|
||||||
return long_id, short_id
|
|
||||||
|
|
||||||
# Utility functions
|
# Utility functions
|
||||||
|
|
||||||
|
def get_user_id(storage):
|
||||||
|
def make_long_id(xpub_hot, xpub_cold):
|
||||||
|
return bitcoin.sha256(''.join(sorted([xpub_hot, xpub_cold])))
|
||||||
|
mpk = storage.get('master_public_keys')
|
||||||
|
xpub1 = mpk["x1/"]
|
||||||
|
xpub2 = mpk["x2/"]
|
||||||
|
long_id = make_long_id(xpub1, xpub2)
|
||||||
|
short_id = hashlib.sha256(long_id).hexdigest()
|
||||||
|
return long_id, short_id
|
||||||
|
|
||||||
def make_xpub(xpub, s):
|
def make_xpub(xpub, s):
|
||||||
_, _, _, c, cK = deserialize_xkey(xpub)
|
_, _, _, c, cK = deserialize_xkey(xpub)
|
||||||
cK2, c2 = bitcoin._CKD_pub(cK, c, s)
|
cK2, c2 = bitcoin._CKD_pub(cK, c, s)
|
||||||
xpub2 = ("0488B21E" + "00" + "00000000" + "00000000").decode("hex") + c2 + cK2
|
xpub2 = ("0488B21E" + "00" + "00000000" + "00000000").decode("hex") + c2 + cK2
|
||||||
return EncodeBase58Check(xpub2)
|
return EncodeBase58Check(xpub2)
|
||||||
|
|
||||||
def restore_third_key(wallet):
|
|
||||||
long_user_id, short_id = wallet.get_user_id()
|
|
||||||
xpub3 = make_xpub(signing_xpub, long_user_id)
|
|
||||||
wallet.add_master_public_key('x3/', xpub3)
|
|
||||||
|
|
||||||
def make_billing_address(wallet, num):
|
def make_billing_address(wallet, num):
|
||||||
long_id, short_id = wallet.get_user_id()
|
long_id, short_id = wallet.get_user_id()
|
||||||
|
@ -324,9 +310,6 @@ class TrustedCoinPlugin(BasePlugin):
|
||||||
def is_available(self):
|
def is_available(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def set_enabled(self, wallet, enabled):
|
|
||||||
wallet.storage.put('use_' + self.name, enabled)
|
|
||||||
|
|
||||||
def is_enabled(self):
|
def is_enabled(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -345,28 +328,42 @@ class TrustedCoinPlugin(BasePlugin):
|
||||||
wallet.price_per_tx = dict(billing_info['price_per_tx'])
|
wallet.price_per_tx = dict(billing_info['price_per_tx'])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def create_extended_seed(self, wallet, wizard):
|
def make_seed(self):
|
||||||
self.wallet = wallet
|
return Mnemonic('english').make_seed(num_bits=256, prefix=SEED_PREFIX)
|
||||||
self.wizard = wizard
|
|
||||||
seed = wallet.make_seed()
|
|
||||||
self.wizard.show_seed_dialog(run_next=wizard.confirm_seed, seed_text=seed)
|
|
||||||
|
|
||||||
def show_disclaimer(self, wallet, wizard):
|
@hook
|
||||||
self.set_enabled(wallet, True)
|
def do_clear(self, window):
|
||||||
|
window.wallet.is_billing = False
|
||||||
|
|
||||||
|
def show_disclaimer(self, wizard):
|
||||||
wizard.set_icon(':icons/trustedcoin.png')
|
wizard.set_icon(':icons/trustedcoin.png')
|
||||||
wizard.stack = []
|
wizard.stack = []
|
||||||
wizard.confirm_dialog('\n\n'.join(DISCLAIMER), run_next = lambda x: wizard.run('create_extended_seed'))
|
wizard.confirm_dialog('\n\n'.join(DISCLAIMER), run_next = lambda x: wizard.run('choose_seed'))
|
||||||
|
|
||||||
def create_wallet(self, wallet, wizard, seed, password):
|
def choose_seed(self, wizard):
|
||||||
wallet.storage.put('seed_version', wallet.seed_version)
|
title = _('Create or restore')
|
||||||
wallet.storage.put('use_encryption', password is not None)
|
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()
|
words = seed.split()
|
||||||
n = len(words)/2
|
n = len(words)/2
|
||||||
wallet.add_xprv_from_seed(' '.join(words[0:n]), 'x1/', password)
|
keystore1 = keystore.xprv_from_seed(' '.join(words[0:n]), password)
|
||||||
wallet.add_xpub_from_seed(' '.join(words[n:]), 'x2/')
|
keystore2 = keystore.xpub_from_seed(' '.join(words[n:]))
|
||||||
wallet.storage.write()
|
keystore1.save(wizard.storage, 'x1/')
|
||||||
|
keystore2.save(wizard.storage, 'x2/')
|
||||||
|
wizard.storage.write()
|
||||||
msg = [
|
msg = [
|
||||||
_("Your wallet file is: %s.")%os.path.abspath(wallet.storage.path),
|
_("Your wallet file is: %s.")%os.path.abspath(wizard.storage.path),
|
||||||
_("You need to be online in order to complete the creation of "
|
_("You need to be online in order to complete the creation of "
|
||||||
"your wallet. If you generated your seed on an offline "
|
"your wallet. If you generated your seed on an offline "
|
||||||
'computer, click on "%s" to close this window, move your '
|
'computer, click on "%s" to close this window, move your '
|
||||||
|
@ -378,41 +375,45 @@ class TrustedCoinPlugin(BasePlugin):
|
||||||
wizard.stack = []
|
wizard.stack = []
|
||||||
wizard.confirm_dialog(msg, run_next = lambda x: wizard.run('create_remote_key'))
|
wizard.confirm_dialog(msg, run_next = lambda x: wizard.run('create_remote_key'))
|
||||||
|
|
||||||
@hook
|
def restore_wallet(self, wizard):
|
||||||
def do_clear(self, window):
|
|
||||||
window.wallet.is_billing = False
|
|
||||||
|
|
||||||
def on_restore_wallet(self, wallet, wizard):
|
|
||||||
assert isinstance(wallet, self.wallet_class)
|
|
||||||
title = _("Restore two-factor Wallet")
|
title = _("Restore two-factor Wallet")
|
||||||
f = lambda x: wizard.run('on_restore_seed', x)
|
f = lambda x: wizard.run('on_restore_seed', x)
|
||||||
wizard.enter_seed_dialog(run_next=f, title=title, message=RESTORE_MSG, is_valid=self.is_valid_seed)
|
wizard.restore_seed_dialog(run_next=f, is_valid=self.is_valid_seed)
|
||||||
|
|
||||||
def on_restore_seed(self, wallet, wizard, seed):
|
def on_restore_seed(self, wizard, seed):
|
||||||
f = lambda x: wizard.run('on_restore_pw', seed, x)
|
f = lambda pw: wizard.run('on_restore_pw', seed, pw)
|
||||||
wizard.request_password(run_next=f)
|
wizard.request_password(run_next=f)
|
||||||
|
|
||||||
def on_restore_pw(self, wallet, wizard, seed, password):
|
def on_restore_pw(self, wizard, seed, password):
|
||||||
wallet.add_seed(seed, password)
|
# FIXME
|
||||||
|
# wallet.add_seed(seed, password)
|
||||||
|
storage = wizard.storage
|
||||||
words = seed.split()
|
words = seed.split()
|
||||||
n = len(words)/2
|
n = len(words)/2
|
||||||
wallet.add_xprv_from_seed(' '.join(words[0:n]), 'x1/', password)
|
keystore1 = keystore.xprv_from_seed(' '.join(words[0:n]), password)
|
||||||
wallet.add_xprv_from_seed(' '.join(words[n:]), 'x2/', password)
|
keystore2 = keystore.xprv_from_seed(' '.join(words[n:]), password)
|
||||||
restore_third_key(wallet)
|
keystore1.save(storage, 'x1/')
|
||||||
|
keystore2.save(storage, 'x2/')
|
||||||
|
long_user_id, short_id = get_user_id(storage)
|
||||||
|
xpub3 = make_xpub(signing_xpub, long_user_id)
|
||||||
|
keystore3 = keystore.from_xpub(xpub3)
|
||||||
|
keystore3.save(storage, 'x3/')
|
||||||
|
wizard.wallet = Wallet(storage)
|
||||||
wizard.create_addresses()
|
wizard.create_addresses()
|
||||||
|
|
||||||
def create_remote_key(self, wallet, window):
|
def create_remote_key(self, wizard):
|
||||||
email = self.accept_terms_of_use(window)
|
email = self.accept_terms_of_use(wizard)
|
||||||
xpub_hot = wallet.master_public_keys["x1/"]
|
mpk = wizard.storage.get('master_public_keys')
|
||||||
xpub_cold = wallet.master_public_keys["x2/"]
|
xpub1 = mpk["x1/"]
|
||||||
|
xpub2 = mpk["x2/"]
|
||||||
# Generate third key deterministically.
|
# Generate third key deterministically.
|
||||||
long_user_id, short_id = wallet.get_user_id()
|
long_user_id, short_id = get_user_id(wizard.storage)
|
||||||
xpub3 = make_xpub(signing_xpub, long_user_id)
|
xpub3 = make_xpub(signing_xpub, long_user_id)
|
||||||
# secret must be sent by the server
|
# secret must be sent by the server
|
||||||
try:
|
try:
|
||||||
r = server.create(xpub_hot, xpub_cold, email)
|
r = server.create(xpub1, xpub2, email)
|
||||||
except socket.error:
|
except socket.error:
|
||||||
window.show_message('Server not reachable, aborting')
|
wizard.show_message('Server not reachable, aborting')
|
||||||
return
|
return
|
||||||
except TrustedCoinException as e:
|
except TrustedCoinException as e:
|
||||||
if e.status_code == 409:
|
if e.status_code == 409:
|
||||||
|
@ -424,7 +425,7 @@ class TrustedCoinPlugin(BasePlugin):
|
||||||
else:
|
else:
|
||||||
otp_secret = r.get('otp_secret')
|
otp_secret = r.get('otp_secret')
|
||||||
if not otp_secret:
|
if not otp_secret:
|
||||||
window.show_message(_('Error'))
|
wizard.show_message(_('Error'))
|
||||||
return
|
return
|
||||||
_xpub3 = r['xpubkey_cosigner']
|
_xpub3 = r['xpubkey_cosigner']
|
||||||
_id = r['id']
|
_id = r['id']
|
||||||
|
@ -432,10 +433,24 @@ class TrustedCoinPlugin(BasePlugin):
|
||||||
assert _id == short_id, ("user id error", _id, short_id)
|
assert _id == short_id, ("user id error", _id, short_id)
|
||||||
assert xpub3 == _xpub3, ("xpub3 error", xpub3, _xpub3)
|
assert xpub3 == _xpub3, ("xpub3 error", xpub3, _xpub3)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
window.show_message(str(e))
|
wizard.show_message(str(e))
|
||||||
return
|
return
|
||||||
if not self.setup_google_auth(window, short_id, otp_secret):
|
if not self.setup_google_auth(wizard, short_id, otp_secret):
|
||||||
window.show_message("otp error")
|
wizard.show_message("otp error")
|
||||||
return
|
return
|
||||||
wallet.add_master_public_key('x3/', xpub3)
|
keystore3 = keystore.from_xpub(xpub3)
|
||||||
window.run('create_addresses')
|
keystore3.save(wizard.storage, 'x3/')
|
||||||
|
wizard.storage.put('use_trustedcoin', True)
|
||||||
|
wizard.storage.write()
|
||||||
|
wizard.wallet = Wallet(wizard.storage)
|
||||||
|
wizard.run('create_addresses')
|
||||||
|
|
||||||
|
@hook
|
||||||
|
def get_action(self, storage):
|
||||||
|
mpk = storage.get('master_public_keys', {})
|
||||||
|
if not mpk.get('x1/'):
|
||||||
|
return self, 'show_disclaimer'
|
||||||
|
if not mpk.get('x2/'):
|
||||||
|
return self, 'show_disclaimer'
|
||||||
|
if not mpk.get('x3/'):
|
||||||
|
return self, 'create_remote_key'
|
||||||
|
|
Loading…
Add table
Reference in a new issue