Merge branch '1.9' of git://github.com/spesmilo/electrum into 1.9

This commit is contained in:
thomasv 2013-08-27 13:59:20 +02:00
commit 238ed35134
11 changed files with 771 additions and 557 deletions

View File

@ -22,6 +22,7 @@ import sys, os, time, json
import optparse import optparse
import platform import platform
from decimal import Decimal from decimal import Decimal
import traceback
try: try:
import ecdsa import ecdsa
@ -106,7 +107,6 @@ if __name__ == '__main__':
util.check_windows_wallet_migration() util.check_windows_wallet_migration()
config = SimpleConfig(config_options) config = SimpleConfig(config_options)
wallet = Wallet(config)
if len(args)==0: if len(args)==0:
@ -124,86 +124,22 @@ if __name__ == '__main__':
try: try:
gui = __import__('electrum_gui.gui_' + gui_name, fromlist=['electrum_gui']) gui = __import__('electrum_gui.gui_' + gui_name, fromlist=['electrum_gui'])
except ImportError: except ImportError:
sys.exit("Error: Unknown GUI: " + gui_name ) traceback.print_exc(file=sys.stdout)
sys.exit()
#sys.exit("Error: Unknown GUI: " + gui_name )
interface = Interface(config, True) gui = gui.ElectrumGui(config)
wallet.interface = interface
gui = gui.ElectrumGui(wallet, config)
found = config.wallet_file_exists
if not found:
a = gui.restore_or_create()
if not a: exit()
if a =='create':
wallet.init_seed(None)
gui.show_seed()
if gui.verify_seed():
wallet.save_seed()
else:
exit()
else:
# ask for seed and gap.
sg = gui.seed_dialog()
if not sg: exit()
seed, gap = sg
if not seed: exit()
wallet.gap_limit = gap
if len(seed) == 128:
wallet.seed = ''
wallet.init_sequence(str(seed))
else:
wallet.init_seed(str(seed))
wallet.save_seed()
# select a server.
s = gui.network_dialog()
if s is None:
config.set_key("server", None, True)
config.set_key('auto_cycle', False, True)
interface.start(wait = False)
interface.send([('server.peers.subscribe',[])])
# generate the first addresses, in case we are offline
if not found and ( s is None or a == 'create'):
wallet.synchronize()
verifier = WalletVerifier(interface, config)
verifier.start()
wallet.set_verifier(verifier)
synchronizer = WalletSynchronizer(wallet, config)
synchronizer.start()
if not found and a == 'restore' and s is not None:
try:
keep_it = gui.restore_wallet()
wallet.fill_addressbook()
except:
import traceback
traceback.print_exc(file=sys.stdout)
exit()
if not keep_it: exit()
if not found:
gui.password_dialog()
#wallet.save()
gui.main(url) gui.main(url)
#wallet.save()
verifier.stop()
synchronizer.stop()
interface.stop()
# we use daemon threads, their termination is enforced. # we use daemon threads, their termination is enforced.
# this sleep command gives them time to terminate cleanly. # this sleep command gives them time to terminate cleanly.
time.sleep(0.1) time.sleep(0.1)
sys.exit(0) sys.exit(0)
# instanciate wallet for command-line
wallet = Wallet(config)
if cmd not in known_commands: if cmd not in known_commands:
cmd = 'help' cmd = 'help'
@ -337,6 +273,16 @@ if __name__ == '__main__':
elif cmd in ['payto', 'mktx']: elif cmd in ['payto', 'mktx']:
domain = [options.from_addr] if options.from_addr else None domain = [options.from_addr] if options.from_addr else None
args = [ 'mktx', args[1], Decimal(args[2]), Decimal(options.tx_fee) if options.tx_fee else None, options.change_addr, domain ] args = [ 'mktx', args[1], Decimal(args[2]), Decimal(options.tx_fee) if options.tx_fee else None, options.change_addr, domain ]
elif cmd in ['paytomany', 'mksendmanytx']:
domain = [options.from_addr] if options.from_addr else None
outputs = []
for i in range(1, len(args), 2):
if len(args) < i+2:
print_msg("Error: Mismatched arguments.")
exit(1)
outputs.append((args[i], Decimal(args[i+1])))
args = [ 'mksendmanytx', outputs, Decimal(options.tx_fee) if options.tx_fee else None, options.change_addr, domain ]
elif cmd == 'help': elif cmd == 'help':
if len(args) < 2: if len(args) < 2:
@ -429,7 +375,8 @@ if __name__ == '__main__':
try: try:
result = func(*args[1:]) result = func(*args[1:])
except BaseException, e: except BaseException, e:
print_msg("Error: " + str(e)) import traceback
traceback.print_exc(file=sys.stdout)
sys.exit(1) sys.exit(1)
if type(result) == str: if type(result) == str:

View File

@ -42,7 +42,7 @@ except:
from electrum.wallet import format_satoshis from electrum.wallet import format_satoshis
from electrum.bitcoin import Transaction, is_valid from electrum.bitcoin import Transaction, is_valid
from electrum import mnemonic from electrum import mnemonic
from electrum import util, bitcoin, commands from electrum import util, bitcoin, commands, Interface, Wallet, WalletVerifier, WalletSynchronizer
import bmp, pyqrnative import bmp, pyqrnative
import exchange_rate import exchange_rate
@ -265,6 +265,7 @@ class ElectrumWindow(QMainWindow):
if reason == QSystemTrayIcon.DoubleClick: if reason == QSystemTrayIcon.DoubleClick:
self.showNormal() self.showNormal()
def __init__(self, wallet, config): def __init__(self, wallet, config):
QMainWindow.__init__(self) QMainWindow.__init__(self)
self._close_electrum = False self._close_electrum = False
@ -352,10 +353,21 @@ class ElectrumWindow(QMainWindow):
wallet_folder = self.wallet.config.path wallet_folder = self.wallet.config.path
re.sub("(\/\w*.dat)$", "", wallet_folder) re.sub("(\/\w*.dat)$", "", wallet_folder)
file_name = QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder, "*.dat") file_name = QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder, "*.dat")
if not file_name: return file_name
return
else: def open_wallet(self):
self.load_wallet(file_name) n = self.select_wallet_file()
if n:
self.load_wallet(n)
def new_wallet(self):
n = self.getOpenFileName("Select wallet file")
wizard = installwizard.InstallWizard(self.config, self.interface)
wallet = wizard.run()
if wallet:
self.load_wallet(wallet)
def init_menubar(self): def init_menubar(self):
@ -363,7 +375,10 @@ class ElectrumWindow(QMainWindow):
electrum_menu = menubar.addMenu(_("&File")) electrum_menu = menubar.addMenu(_("&File"))
open_wallet_action = electrum_menu.addAction(_("Open wallet")) open_wallet_action = electrum_menu.addAction(_("Open wallet"))
open_wallet_action.triggered.connect(self.select_wallet_file) open_wallet_action.triggered.connect(self.open_wallet)
new_wallet_action = electrum_menu.addAction(_("New wallet"))
new_wallet_action.triggered.connect(self.new_wallet)
preferences_name = _("Preferences") preferences_name = _("Preferences")
if sys.platform == 'darwin': if sys.platform == 'darwin':
@ -430,6 +445,7 @@ class ElectrumWindow(QMainWindow):
self.setMenuBar(menubar) self.setMenuBar(menubar)
def load_wallet(self, filename): def load_wallet(self, filename):
import electrum import electrum
@ -1268,7 +1284,7 @@ class ElectrumWindow(QMainWindow):
account_items = [] account_items = []
for k, account in account_items: for k, account in account_items:
name = account.get_name() name = self.wallet.labels.get(k, 'unnamed account')
c,u = self.wallet.get_account_balance(k) c,u = self.wallet.get_account_balance(k)
account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] ) account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
l.addTopLevelItem(account_item) l.addTopLevelItem(account_item)
@ -1395,7 +1411,7 @@ class ElectrumWindow(QMainWindow):
sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) ) sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
if self.wallet.seed: if self.wallet.seed:
self.lock_icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png") self.lock_icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
self.password_button = StatusBarButton( self.lock_icon, _("Password"), lambda: self.change_password_dialog(self.wallet, self) ) self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
sb.addPermanentWidget( self.password_button ) sb.addPermanentWidget( self.password_button )
sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) ) sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
if self.wallet.seed: if self.wallet.seed:
@ -1406,6 +1422,13 @@ class ElectrumWindow(QMainWindow):
self.run_hook('create_status_bar', (sb,)) self.run_hook('create_status_bar', (sb,))
self.setStatusBar(sb) self.setStatusBar(sb)
def change_password_dialog(self):
from password_dialog import PasswordDialog
d = PasswordDialog(self.wallet, self)
d.run()
def go_lite(self): def go_lite(self):
import gui_lite import gui_lite
@ -1490,63 +1513,12 @@ class ElectrumWindow(QMainWindow):
except: except:
QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK')) QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
return return
self.show_seed(seed, self.wallet.imported_keys, self)
from seed_dialog import SeedDialog
d = SeedDialog(self)
d.show_seed(seed, self.wallet.imported_keys)
@classmethod
def show_seed(self, seed, imported_keys, parent=None):
dialog = QDialog(parent)
dialog.setModal(1)
dialog.setWindowTitle('Electrum' + ' - ' + _('Seed'))
brainwallet = ' '.join(mnemonic.mn_encode(seed))
label1 = QLabel(_("Your wallet generation seed is")+ ":")
seed_text = QTextEdit(brainwallet)
seed_text.setReadOnly(True)
seed_text.setMaximumHeight(130)
msg2 = _("Please write down or memorize these 12 words (order is important).") + " " \
+ _("This seed will allow you to recover your wallet in case of computer failure.") + " " \
+ _("Your seed is also displayed as QR code, in case you want to transfer it to a mobile phone.") + "<p>" \
+ "<b>"+_("WARNING")+":</b> " + _("Never disclose your seed. Never type it on a website.") + "</b><p>"
if imported_keys:
msg2 += "<b>"+_("WARNING")+":</b> " + _("Your wallet contains imported keys. These keys cannot be recovered from seed.") + "</b><p>"
label2 = QLabel(msg2)
label2.setWordWrap(True)
logo = QLabel()
logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
logo.setMaximumWidth(60)
qrw = QRCodeWidget(seed)
ok_button = QPushButton(_("OK"))
ok_button.setDefault(True)
ok_button.clicked.connect(dialog.accept)
grid = QGridLayout()
#main_layout.addWidget(logo, 0, 0)
grid.addWidget(logo, 0, 0)
grid.addWidget(label1, 0, 1)
grid.addWidget(seed_text, 1, 0, 1, 2)
grid.addWidget(qrw, 0, 2, 2, 1)
vbox = QVBoxLayout()
vbox.addLayout(grid)
vbox.addWidget(label2)
hbox = QHBoxLayout()
hbox.addStretch(1)
hbox.addWidget(ok_button)
vbox.addLayout(hbox)
dialog.setLayout(vbox)
dialog.exec_()
def show_qrcode(self, data, title = "QR code"): def show_qrcode(self, data, title = "QR code"):
if not data: return if not data: return
@ -1728,79 +1700,6 @@ class ElectrumWindow(QMainWindow):
@staticmethod
def change_password_dialog( wallet, parent=None ):
if not wallet.seed:
QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
return
d = QDialog(parent)
d.setModal(1)
pw = QLineEdit()
pw.setEchoMode(2)
new_pw = QLineEdit()
new_pw.setEchoMode(2)
conf_pw = QLineEdit()
conf_pw.setEchoMode(2)
vbox = QVBoxLayout()
if parent:
msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'\
+_('To disable wallet encryption, enter an empty new password.')) \
if wallet.use_encryption else _('Your wallet keys are not encrypted')
else:
msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
+_("Leave these fields empty if you want to disable encryption.")
vbox.addWidget(QLabel(msg))
grid = QGridLayout()
grid.setSpacing(8)
if wallet.use_encryption:
grid.addWidget(QLabel(_('Password')), 1, 0)
grid.addWidget(pw, 1, 1)
grid.addWidget(QLabel(_('New Password')), 2, 0)
grid.addWidget(new_pw, 2, 1)
grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
grid.addWidget(conf_pw, 3, 1)
vbox.addLayout(grid)
vbox.addLayout(ok_cancel_buttons(d))
d.setLayout(vbox)
if not d.exec_(): return
password = unicode(pw.text()) if wallet.use_encryption else None
new_password = unicode(new_pw.text())
new_password2 = unicode(conf_pw.text())
try:
seed = wallet.decode_seed(password)
except:
QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
return
if new_password != new_password2:
QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
return ElectrumWindow.change_password_dialog(wallet, parent) # Retry
try:
wallet.update_password(seed, password, new_password)
except:
QMessageBox.warning(parent, _('Error'), _('Failed to update password'), _('OK'))
return
QMessageBox.information(parent, _('Success'), _('Password was updated successfully'), _('OK'))
if parent:
icon = QIcon(":icons/lock.png") if wallet.use_encryption else QIcon(":icons/unlock.png")
parent.password_button.setIcon( icon )
def generate_transaction_information_widget(self, tx): def generate_transaction_information_widget(self, tx):
tabs = QTabWidget(self) tabs = QTabWidget(self)
@ -2282,10 +2181,13 @@ class OpenFileEventFilter(QObject):
return True return True
return False return False
class ElectrumGui: class ElectrumGui:
def __init__(self, wallet, config, app=None): def __init__(self, config, app=None):
self.wallet = wallet self.interface = Interface(config, True)
self.config = config self.config = config
self.windows = [] self.windows = []
self.efilter = OpenFileEventFilter(self.windows) self.efilter = OpenFileEventFilter(self.windows)
@ -2293,116 +2195,32 @@ class ElectrumGui:
self.app = QApplication(sys.argv) self.app = QApplication(sys.argv)
self.app.installEventFilter(self.efilter) self.app.installEventFilter(self.efilter)
def restore_or_create(self):
msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
if r==2: return None
return 'restore' if r==1 else 'create'
def main(self, url):
def verify_seed(self):
r = self.seed_dialog(False) found = self.config.wallet_file_exists
if r != self.wallet.seed: if not found:
QMessageBox.warning(None, _('Error'), 'incorrect seed', 'OK') import installwizard
return False wizard = installwizard.InstallWizard(self.config, self.interface)
wallet = wizard.run()
if not wallet:
exit()
else: else:
return True wallet = Wallet(self.config)
self.wallet = wallet
self.interface.start(wait = False)
self.interface.send([('server.peers.subscribe',[])])
wallet.interface = self.interface
verifier = WalletVerifier(self.interface, self.config)
verifier.start()
wallet.set_verifier(verifier)
synchronizer = WalletSynchronizer(wallet, self.config)
synchronizer.start()
def seed_dialog(self, is_restore=True):
d = QDialog()
d.setModal(1)
vbox = QVBoxLayout()
if is_restore:
msg = _("Please enter your wallet seed (or your master public key if you want to create a watching-only wallet)." + ' ')
else:
msg = _("Your seed is important! To make sure that you have properly saved your seed, please type it here." + ' ')
msg += _("Your seed can be entered as a sequence of words, or as a hexadecimal string."+ '\n')
label=QLabel(msg)
label.setWordWrap(True)
vbox.addWidget(label)
seed_e = QTextEdit()
seed_e.setMaximumHeight(100)
vbox.addWidget(seed_e)
if is_restore:
grid = QGridLayout()
grid.setSpacing(8)
gap_e = AmountEdit(None, True)
gap_e.setText("5")
grid.addWidget(QLabel(_('Gap limit')), 2, 0)
grid.addWidget(gap_e, 2, 1)
grid.addWidget(HelpButton(_('Keep the default value unless you modified this parameter in your wallet.')), 2, 3)
vbox.addLayout(grid)
vbox.addLayout(ok_cancel_buttons(d))
d.setLayout(vbox)
if not d.exec_(): return
try:
seed = str(seed_e.toPlainText())
seed.decode('hex')
except:
try:
seed = mnemonic.mn_decode( seed.split() )
except:
QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
return
if not seed:
QMessageBox.warning(None, _('Error'), _('No seed'), _('OK'))
return
if not is_restore:
return seed
else:
try:
gap = int(unicode(gap_e.text()))
except:
QMessageBox.warning(None, _('Error'), 'error', 'OK')
return
return seed, gap
def network_dialog(self):
return NetworkDialog(self.wallet.interface, self.config, None).do_exec()
def show_seed(self):
ElectrumWindow.show_seed(self.wallet.seed, self.wallet.imported_keys)
def password_dialog(self):
if self.wallet.seed:
ElectrumWindow.change_password_dialog(self.wallet)
def restore_wallet(self):
wallet = self.wallet
# wait until we are connected, because the user might have selected another server
if not wallet.interface.is_connected:
waiting = lambda: False if wallet.interface.is_connected else "%s \n" % (_("Connecting..."))
waiting_dialog(waiting)
waiting = lambda: False if wallet.is_up_to_date() else "%s\n%s %d\n%s %.1f"\
%(_("Please wait..."),_("Addresses generated:"),len(wallet.addresses(True)),_("Kilobytes received:"), wallet.interface.bytes_received/1024.)
wallet.set_up_to_date(False)
wallet.interface.poke('synchronizer')
waiting_dialog(waiting)
if wallet.is_found():
print_error( "Recovery successful" )
else:
QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
return True
def main(self,url):
s = Timer() s = Timer()
s.start() s.start()
w = ElectrumWindow(self.wallet, self.config) w = ElectrumWindow(self.wallet, self.config)
@ -2415,4 +2233,8 @@ class ElectrumGui:
self.app.exec_() self.app.exec_()
verifier.stop()
synchronizer.stop()
self.interface.stop()

183
gui/installwizard.py Normal file
View File

@ -0,0 +1,183 @@
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import PyQt4.QtCore as QtCore
from i18n import _
from electrum import Wallet, mnemonic
from seed_dialog import SeedDialog
from network_dialog import NetworkDialog
from qt_util import *
class InstallWizard(QDialog):
def __init__(self, config, interface):
QDialog.__init__(self)
self.config = config
self.interface = interface
def restore_or_create(self):
msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
if r==2: return None
return 'restore' if r==1 else 'create'
def verify_seed(self, wallet):
r = self.seed_dialog(False)
if r != wallet.seed:
QMessageBox.warning(None, _('Error'), 'incorrect seed', 'OK')
return False
else:
return True
def seed_dialog(self, is_restore=True):
d = QDialog()
d.setModal(1)
vbox = QVBoxLayout()
if is_restore:
msg = _("Please enter your wallet seed (or your master public key if you want to create a watching-only wallet)." + ' ')
else:
msg = _("Your seed is important! To make sure that you have properly saved your seed, please type it here." + ' ')
msg += _("Your seed can be entered as a sequence of words, or as a hexadecimal string."+ '\n')
label=QLabel(msg)
label.setWordWrap(True)
vbox.addWidget(label)
seed_e = QTextEdit()
seed_e.setMaximumHeight(100)
vbox.addWidget(seed_e)
if is_restore:
grid = QGridLayout()
grid.setSpacing(8)
gap_e = AmountEdit(None, True)
gap_e.setText("5")
grid.addWidget(QLabel(_('Gap limit')), 2, 0)
grid.addWidget(gap_e, 2, 1)
grid.addWidget(HelpButton(_('Keep the default value unless you modified this parameter in your wallet.')), 2, 3)
vbox.addLayout(grid)
vbox.addLayout(ok_cancel_buttons(d))
d.setLayout(vbox)
if not d.exec_(): return
try:
seed = str(seed_e.toPlainText())
seed.decode('hex')
except:
try:
seed = mnemonic.mn_decode( seed.split() )
except:
QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
return
if not seed:
QMessageBox.warning(None, _('Error'), _('No seed'), _('OK'))
return
if not is_restore:
return seed
else:
try:
gap = int(unicode(gap_e.text()))
except:
QMessageBox.warning(None, _('Error'), 'error', 'OK')
return
return seed, gap
def network_dialog(self):
return NetworkDialog(self.interface, self.config, None).do_exec()
def show_seed(self, wallet):
d = SeedDialog()
d.show_seed(wallet.seed, wallet.imported_keys)
def password_dialog(self, wallet):
from password_dialog import PasswordDialog
d = PasswordDialog(wallet)
d.run()
def restore_wallet(self):
wallet = self.wallet
# wait until we are connected, because the user might have selected another server
if not wallet.interface.is_connected:
waiting = lambda: False if wallet.interface.is_connected else "%s \n" % (_("Connecting..."))
waiting_dialog(waiting)
waiting = lambda: False if wallet.is_up_to_date() else "%s\n%s %d\n%s %.1f"\
%(_("Please wait..."),_("Addresses generated:"),len(wallet.addresses(True)),_("Kilobytes received:"), wallet.interface.bytes_received/1024.)
wallet.set_up_to_date(False)
wallet.interface.poke('synchronizer')
waiting_dialog(waiting)
if wallet.is_found():
print_error( "Recovery successful" )
else:
QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
return True
def run(self):
a = self.restore_or_create()
if not a: exit()
wallet = Wallet(self.config)
wallet.interface = self.interface
if a =='create':
wallet.init_seed(None)
self.show_seed(wallet)
if self.verify_seed(wallet):
wallet.save_seed()
else:
exit()
else:
# ask for seed and gap.
sg = gui.seed_dialog()
if not sg: exit()
seed, gap = sg
if not seed: exit()
wallet.gap_limit = gap
if len(seed) == 128:
wallet.seed = ''
wallet.init_sequence(str(seed))
else:
wallet.init_seed(str(seed))
wallet.save_seed()
# select a server.
s = self.network_dialog()
if s is None:
self.config.set_key("server", None, True)
self.config.set_key('auto_cycle', False, True)
# generate the first addresses, in case we are offline
if s is None or a == 'create':
wallet.synchronize()
if a == 'restore' and s is not None:
try:
keep_it = gui.restore_wallet()
wallet.fill_addressbook()
except:
import traceback
traceback.print_exc(file=sys.stdout)
exit()
if not keep_it: exit()
self.password_dialog(wallet)

104
gui/password_dialog.py Normal file
View File

@ -0,0 +1,104 @@
#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2013 ecdsa@github
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from i18n import _
from qt_util import *
class PasswordDialog(QDialog):
def __init__(self, wallet, parent=None):
QDialog.__init__(self, parent)
self.setModal(1)
self.wallet = wallet
self.parent = parent
self.pw = QLineEdit()
self.pw.setEchoMode(2)
self.new_pw = QLineEdit()
self.new_pw.setEchoMode(2)
self.conf_pw = QLineEdit()
self.conf_pw.setEchoMode(2)
vbox = QVBoxLayout()
if parent:
msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'\
+_('To disable wallet encryption, enter an empty new password.')) \
if wallet.use_encryption else _('Your wallet keys are not encrypted')
else:
msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
+_("Leave these fields empty if you want to disable encryption.")
vbox.addWidget(QLabel(msg))
grid = QGridLayout()
grid.setSpacing(8)
if wallet.use_encryption:
grid.addWidget(QLabel(_('Password')), 1, 0)
grid.addWidget(self.pw, 1, 1)
grid.addWidget(QLabel(_('New Password')), 2, 0)
grid.addWidget(self.new_pw, 2, 1)
grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
grid.addWidget(self.conf_pw, 3, 1)
vbox.addLayout(grid)
vbox.addLayout(ok_cancel_buttons(self))
self.setLayout(vbox)
def run(self):
wallet = self.wallet
if not wallet.seed:
QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
return
if not self.exec_(): return
password = unicode(self.pw.text()) if wallet.use_encryption else None
new_password = unicode(self.new_pw.text())
new_password2 = unicode(self.conf_pw.text())
try:
seed = wallet.decode_seed(password)
except:
QMessageBox.warning(self.parent, _('Error'), _('Incorrect Password'), _('OK'))
return
if new_password != new_password2:
QMessageBox.warning(self.parent, _('Error'), _('Passwords do not match'), _('OK'))
self.run() # Retry
try:
wallet.update_password(seed, password, new_password)
except:
QMessageBox.warning(self.parent, _('Error'), _('Failed to update password'), _('OK'))
return
QMessageBox.information(self.parent, _('Success'), _('Password was updated successfully'), _('OK'))
if self.parent:
icon = QIcon(":icons/lock.png") if wallet.use_encryption else QIcon(":icons/unlock.png")
self.parent.password_button.setIcon( icon )

View File

@ -4,6 +4,7 @@ class BasePlugin:
def __init__(self, gui, name): def __init__(self, gui, name):
self.gui = gui self.gui = gui
self.wallet = self.gui.wallet
self.name = name self.name = name
self.config = gui.config self.config = gui.config

82
gui/seed_dialog.py Normal file
View File

@ -0,0 +1,82 @@
#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2013 ecdsa@github
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import PyQt4.QtCore as QtCore
from i18n import _
from electrum import mnemonic
from qrcodewidget import QRCodeWidget
class SeedDialog(QDialog):
def __init__(self, parent=None):
QDialog.__init__(self, parent)
self.setModal(1)
self.setWindowTitle('Electrum' + ' - ' + _('Seed'))
def show_seed(self, seed, imported_keys, parent=None):
brainwallet = ' '.join(mnemonic.mn_encode(seed))
label1 = QLabel(_("Your wallet generation seed is")+ ":")
seed_text = QTextEdit(brainwallet)
seed_text.setReadOnly(True)
seed_text.setMaximumHeight(130)
msg2 = _("Please write down or memorize these 12 words (order is important).") + " " \
+ _("This seed will allow you to recover your wallet in case of computer failure.") + " " \
+ _("Your seed is also displayed as QR code, in case you want to transfer it to a mobile phone.") + "<p>" \
+ "<b>"+_("WARNING")+":</b> " + _("Never disclose your seed. Never type it on a website.") + "</b><p>"
if imported_keys:
msg2 += "<b>"+_("WARNING")+":</b> " + _("Your wallet contains imported keys. These keys cannot be recovered from seed.") + "</b><p>"
label2 = QLabel(msg2)
label2.setWordWrap(True)
logo = QLabel()
logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
logo.setMaximumWidth(60)
qrw = QRCodeWidget(seed)
ok_button = QPushButton(_("OK"))
ok_button.setDefault(True)
ok_button.clicked.connect(self.accept)
grid = QGridLayout()
#main_layout.addWidget(logo, 0, 0)
grid.addWidget(logo, 0, 0)
grid.addWidget(label1, 0, 1)
grid.addWidget(seed_text, 1, 0, 1, 2)
grid.addWidget(qrw, 0, 2, 2, 1)
vbox = QVBoxLayout()
vbox.addLayout(grid)
vbox.addWidget(label2)
hbox = QHBoxLayout()
hbox.addStretch(1)
hbox.addWidget(ok_button)
vbox.addLayout(hbox)
self.setLayout(vbox)
self.exec_()

View File

@ -24,13 +24,9 @@ class Account(object):
def __init__(self, v): def __init__(self, v):
self.addresses = v.get('0', []) self.addresses = v.get('0', [])
self.change = v.get('1', []) self.change = v.get('1', [])
self.name = v.get('name', 'unnamed')
def dump(self): def dump(self):
return {'0':self.addresses, '1':self.change, 'name':self.name} return {'0':self.addresses, '1':self.change}
def get_name(self):
return self.name
def get_addresses(self, for_change): def get_addresses(self, for_change):
return self.change[:] if for_change else self.addresses[:] return self.change[:] if for_change else self.addresses[:]
@ -171,25 +167,9 @@ class BIP32_Account(Account):
K, K_compressed, chain = CKD_prime(K, chain, i) K, K_compressed, chain = CKD_prime(K, chain, i)
return K_compressed.encode('hex') return K_compressed.encode('hex')
def get_private_key(self, sequence, master_k): def redeem_script(self, sequence):
chain = self.c return None
k = master_k
for i in sequence:
k, chain = CKD(k, chain, i)
return SecretToASecret(k, True)
def get_private_keys(self, sequence_list, seed):
return [ self.get_private_key( sequence, seed) for sequence in sequence_list]
def check_seed(self, seed):
master_secret, master_chain, master_public_key, master_public_key_compressed = bip32_init(seed)
assert self.mpk == (master_public_key.encode('hex'), master_chain.encode('hex'))
def get_input_info(self, sequence):
chain, i = sequence
pk_addr = self.get_address(chain, i)
redeemScript = None
return pk_addr, redeemScript
@ -215,18 +195,44 @@ class BIP32_Account_2of2(BIP32_Account):
K, K_compressed, chain = CKD_prime(K, chain, i) K, K_compressed, chain = CKD_prime(K, chain, i)
return K_compressed.encode('hex') return K_compressed.encode('hex')
def get_address(self, for_change, n): def redeem_script(self, sequence):
pubkey1 = self.get_pubkey(for_change, n)
pubkey2 = self.get_pubkey2(for_change, n)
address = Transaction.multisig_script([pubkey1, pubkey2], 2)["address"]
return address
def get_input_info(self, sequence):
chain, i = sequence chain, i = sequence
pubkey1 = self.get_pubkey(chain, i) pubkey1 = self.get_pubkey(chain, i)
pubkey2 = self.get_pubkey2(chain, i) pubkey2 = self.get_pubkey2(chain, i)
# fixme return Transaction.multisig_script([pubkey1, pubkey2], 2)
pk_addr = None # public_key_to_bc_address( pubkey1 ) # we need to return that address to get the right private key
redeemScript = Transaction.multisig_script([pubkey1, pubkey2], 2)['redeemScript'] def get_address(self, for_change, n):
return pk_addr, redeemScript address = hash_160_to_bc_address(hash_160(self.redeem_script((for_change, n)).decode('hex')), 5)
return address
class BIP32_Account_2of3(BIP32_Account_2of2):
def __init__(self, v):
BIP32_Account_2of2.__init__(self, v)
self.c3 = v['c3'].decode('hex')
self.K3 = v['K3'].decode('hex')
self.cK3 = v['cK3'].decode('hex')
def dump(self):
d = BIP32_Account_2of2.dump(self)
d['c3'] = self.c3.encode('hex')
d['K3'] = self.K3.encode('hex')
d['cK3'] = self.cK3.encode('hex')
return d
def get_pubkey3(self, for_change, n):
K = self.K3
chain = self.c3
for i in [for_change, n]:
K, K_compressed, chain = CKD_prime(K, chain, i)
return K_compressed.encode('hex')
def get_redeem_script(self, sequence):
chain, i = sequence
pubkey1 = self.get_pubkey(chain, i)
pubkey2 = self.get_pubkey2(chain, i)
pubkey3 = self.get_pubkey3(chain, i)
return Transaction.multisig_script([pubkey1, pubkey2, pubkey3], 3)

View File

@ -244,17 +244,17 @@ def is_compressed(sec):
return len(b) == 33 return len(b) == 33
def address_from_private_key(sec): def public_key_from_private_key(sec):
# rebuild public key from private key, compressed or uncompressed # rebuild public key from private key, compressed or uncompressed
pkey = regenerate_key(sec) pkey = regenerate_key(sec)
assert pkey assert pkey
# figure out if private key is compressed
compressed = is_compressed(sec) compressed = is_compressed(sec)
# rebuild private and public key from regenerated secret
private_key = GetPrivKey(pkey, compressed)
public_key = GetPubKey(pkey.pubkey, compressed) public_key = GetPubKey(pkey.pubkey, compressed)
return public_key.encode('hex')
def address_from_private_key(sec):
public_key = public_key_from_private_key(sec)
address = public_key_to_bc_address(public_key) address = public_key_to_bc_address(public_key)
return address return address
@ -448,6 +448,11 @@ def bip32_public_derivation(c, K, branch, sequence):
return c.encode('hex'), K.encode('hex'), cK.encode('hex') return c.encode('hex'), K.encode('hex'), cK.encode('hex')
def bip32_private_key(sequence, k, chain):
for i in sequence:
k, chain = CKD(k, chain, i)
return SecretToASecret(k, True)
@ -508,8 +513,7 @@ class Transaction:
raise raise
s += 'ae' s += 'ae'
out = { "address": hash_160_to_bc_address(hash_160(s.decode('hex')), 5), "redeemScript":s } return s
return out
@classmethod @classmethod
def serialize( klass, inputs, outputs, for_sig = None ): def serialize( klass, inputs, outputs, for_sig = None ):
@ -522,24 +526,24 @@ class Transaction:
s += int_to_hex(txin['index'],4) # prev index s += int_to_hex(txin['index'],4) # prev index
if for_sig is None: if for_sig is None:
pubkeysig = txin.get('pubkeysig') signatures = txin['signatures']
if pubkeysig: pubkeys = txin['pubkeys']
pubkey, sig = pubkeysig[0] if not txin.get('redeemScript'):
sig = sig + chr(1) # hashtype pubkey = pubkeys[0]
script = op_push( len(sig)) sig = signatures[0]
script += sig.encode('hex') sig = sig + '01' # hashtype
script += op_push( len(pubkey)) script = op_push(len(sig)/2)
script += pubkey.encode('hex') script += sig
script += op_push(len(pubkey)/2)
script += pubkey
else: else:
signatures = txin['signatures']
pubkeys = txin['pubkeys']
script = '00' # op_0 script = '00' # op_0
for sig in signatures: for sig in signatures:
sig = sig + '01' sig = sig + '01'
script += op_push(len(sig)/2) script += op_push(len(sig)/2)
script += sig script += sig
redeem_script = klass.multisig_script(pubkeys,2).get('redeemScript') redeem_script = klass.multisig_script(pubkeys,2)
script += op_push(len(redeem_script)/2) script += op_push(len(redeem_script)/2)
script += redeem_script script += redeem_script
@ -587,79 +591,47 @@ class Transaction:
def hash(self): def hash(self):
return Hash(self.raw.decode('hex') )[::-1].encode('hex') return Hash(self.raw.decode('hex') )[::-1].encode('hex')
def sign(self, private_keys):
def sign(self, keypairs):
import deserialize import deserialize
is_complete = True
print_error("tx.sign(), keypairs:", keypairs)
for i in range(len(self.inputs)): for i, txin in enumerate(self.inputs):
txin = self.inputs[i]
tx_for_sig = self.serialize( self.inputs, self.outputs, for_sig = i )
# if the input is multisig, parse redeem script
redeem_script = txin.get('redeemScript') redeem_script = txin.get('redeemScript')
if redeem_script: num, redeem_pubkeys = deserialize.parse_redeemScript(redeem_script) if redeem_script else (1, [txin.get('redeemPubkey')])
# 1 parse the redeem script
num, redeem_pubkeys = deserialize.parse_redeemScript(redeem_script)
self.inputs[i]["pubkeys"] = redeem_pubkeys
# build list of public/private keys # add pubkeys
keypairs = {} txin["pubkeys"] = redeem_pubkeys
for sec in private_keys.values(): # get list of already existing signatures
signatures = txin.get("signatures",[])
# continue if this txin is complete
if len(signatures) == num:
continue
tx_for_sig = self.serialize( self.inputs, self.outputs, for_sig = i )
for pubkey in redeem_pubkeys:
# check if we have the corresponding private key
if pubkey in keypairs.keys():
# add signature
sec = keypairs[pubkey]
compressed = is_compressed(sec) compressed = is_compressed(sec)
pkey = regenerate_key(sec) pkey = regenerate_key(sec)
pubkey = GetPubKey(pkey.pubkey, compressed) secexp = pkey.secret
keypairs[ pubkey.encode('hex') ] = sec private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
public_key = private_key.get_verifying_key()
print "keypairs", keypairs sig = private_key.sign_digest( Hash( tx_for_sig.decode('hex') ), sigencode = ecdsa.util.sigencode_der )
print redeem_script, redeem_pubkeys assert public_key.verify_digest( sig, Hash( tx_for_sig.decode('hex') ), sigdecode = ecdsa.util.sigdecode_der)
signatures.append( sig.encode('hex') )
# list of already existing signatures print_error("adding signature for", pubkey)
signatures = txin.get("signatures",[])
print_error("signatures",signatures) txin["signatures"] = signatures
is_complete = is_complete and len(signatures) == num
for pubkey in redeem_pubkeys:
# here we have compressed key.. it won't work
#public_key = ecdsa.VerifyingKey.from_string(pubkey[2:].decode('hex'), curve = SECP256k1)
#for s in signatures:
# try:
# public_key.verify_digest( s.decode('hex')[:-1], Hash( tx_for_sig.decode('hex') ), sigdecode = ecdsa.util.sigdecode_der)
# break
# except ecdsa.keys.BadSignatureError:
# continue
#else:
if 1:
# check if we have a key corresponding to the redeem script
if pubkey in keypairs.keys():
# add signature
sec = keypairs[pubkey]
compressed = is_compressed(sec)
pkey = regenerate_key(sec)
secexp = pkey.secret
private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
public_key = private_key.get_verifying_key()
sig = private_key.sign_digest( Hash( tx_for_sig.decode('hex') ), sigencode = ecdsa.util.sigencode_der )
assert public_key.verify_digest( sig, Hash( tx_for_sig.decode('hex') ), sigdecode = ecdsa.util.sigdecode_der)
signatures.append( sig.encode('hex') )
# for p2sh, pubkeysig is a tuple (may be incomplete)
self.inputs[i]["signatures"] = signatures
print_error("signatures",signatures)
self.is_complete = len(signatures) == num
else:
sec = private_keys[txin['address']]
compressed = is_compressed(sec)
pkey = regenerate_key(sec)
secexp = pkey.secret
private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
public_key = private_key.get_verifying_key()
pkey = EC_KEY(secexp)
pubkey = GetPubKey(pkey.pubkey, compressed)
sig = private_key.sign_digest( Hash( tx_for_sig.decode('hex') ), sigencode = ecdsa.util.sigencode_der )
assert public_key.verify_digest( sig, Hash( tx_for_sig.decode('hex') ), sigdecode = ecdsa.util.sigdecode_der)
self.inputs[i]["pubkeysig"] = [(pubkey, sig)]
self.is_complete = True
self.is_complete = is_complete
self.raw = self.serialize( self.inputs, self.outputs ) self.raw = self.serialize( self.inputs, self.outputs )

View File

@ -60,7 +60,9 @@ register_command('importprivkey', 1, 1, True, True, 'Import a private k
register_command('listaddresses', 3, 3, False, True, 'Returns your list of addresses.', '', listaddr_options) register_command('listaddresses', 3, 3, False, True, 'Returns your list of addresses.', '', listaddr_options)
register_command('listunspent', 0, 0, False, True, 'Returns a list of unspent inputs in your wallet.') register_command('listunspent', 0, 0, False, True, 'Returns a list of unspent inputs in your wallet.')
register_command('mktx', 5, 5, True, True, 'Create a signed transaction', 'mktx <recipient> <amount> [label]', payto_options) register_command('mktx', 5, 5, True, True, 'Create a signed transaction', 'mktx <recipient> <amount> [label]', payto_options)
register_command('mksendmanytx', 4, 4, True, True, 'Create a signed transaction', 'mksendmanytx <recipient> <amount> [<recipient> <amount> ...]', payto_options)
register_command('payto', 5, 5, True, False, 'Create and broadcast a transaction.', "payto <recipient> <amount> [label]\n<recipient> can be a bitcoin address or a label", payto_options) register_command('payto', 5, 5, True, False, 'Create and broadcast a transaction.', "payto <recipient> <amount> [label]\n<recipient> can be a bitcoin address or a label", payto_options)
register_command('paytomany', 4, 4, True, False, 'Create and broadcast a transaction.', "paytomany <recipient> <amount> [<recipient> <amount> ...]\n<recipient> can be a bitcoin address or a label", payto_options)
register_command('password', 0, 0, True, True, 'Change your password') register_command('password', 0, 0, True, True, 'Change your password')
register_command('prioritize', 1, 1, False, True, 'Coins at prioritized addresses are spent first.', 'prioritize <address>') register_command('prioritize', 1, 1, False, True, 'Coins at prioritized addresses are spent first.', 'prioritize <address>')
register_command('restore', 0, 0, False, False, 'Restore a wallet', '', restore_options) register_command('restore', 0, 0, False, False, 'Restore a wallet', '', restore_options)
@ -131,7 +133,9 @@ class Commands:
def createmultisig(self, num, pubkeys): def createmultisig(self, num, pubkeys):
assert isinstance(pubkeys, list) assert isinstance(pubkeys, list)
return Transaction.multisig_script(pubkeys, num) redeem_script = Transaction.multisig_script(pubkeys, num)
address = hash_160_to_bc_address(hash_160(redeem_script.decode('hex')), 5)
return {'address':address, 'redeemScript':redeem_script}
def freeze(self,addr): def freeze(self,addr):
return self.wallet.freeze(addr) return self.wallet.freeze(addr)
@ -205,10 +209,11 @@ class Commands:
return self.wallet.verify_message(address, signature, message) return self.wallet.verify_message(address, signature, message)
def _mktx(self, to_address, amount, fee = None, change_addr = None, domain = None): def _mktx(self, outputs, fee = None, change_addr = None, domain = None):
if not is_valid(to_address): for to_address, amount in outputs:
raise BaseException("Invalid Bitcoin address", to_address) if not is_valid(to_address):
raise BaseException("Invalid Bitcoin address", to_address)
if change_addr: if change_addr:
if not is_valid(change_addr): if not is_valid(change_addr):
@ -223,25 +228,40 @@ class Commands:
raise BaseException("address not in wallet", addr) raise BaseException("address not in wallet", addr)
for k, v in self.wallet.labels.items(): for k, v in self.wallet.labels.items():
if v == to_address:
to_address = k
print_msg("alias", to_address)
break
if change_addr and v == change_addr: if change_addr and v == change_addr:
change_addr = k change_addr = k
amount = int(100000000*amount) final_outputs = []
for to_address, amount in outputs:
for k, v in self.wallet.labels.items():
if v == to_address:
to_address = k
print_msg("alias", to_address)
break
amount = int(100000000*amount)
final_outputs.append((to_address, amount))
if fee: fee = int(100000000*fee) if fee: fee = int(100000000*fee)
return self.wallet.mktx( [(to_address, amount)], self.password, fee , change_addr, domain) return self.wallet.mktx(final_outputs, self.password, fee , change_addr, domain)
def mktx(self, to_address, amount, fee = None, change_addr = None, domain = None): def mktx(self, to_address, amount, fee = None, change_addr = None, domain = None):
tx = self._mktx(to_address, amount, fee, change_addr, domain) tx = self._mktx([(to_address, amount)], fee, change_addr, domain)
return tx.as_dict()
def mksendmanytx(self, outputs, fee = None, change_addr = None, domain = None):
tx = self._mktx(outputs, fee, change_addr, domain)
return tx.as_dict() return tx.as_dict()
def payto(self, to_address, amount, fee = None, change_addr = None, domain = None): def payto(self, to_address, amount, fee = None, change_addr = None, domain = None):
tx = self._mktx(to_address, amount, fee, change_addr, domain) tx = self._mktx([(to_address, amount)], fee, change_addr, domain)
r, h = self.wallet.sendtx( tx )
return h
def paytomany(self, outputs, fee = None, change_addr = None, domain = None):
tx = self._mktx(outputs, fee, change_addr, domain)
r, h = self.wallet.sendtx( tx ) r, h = self.wallet.sendtx( tx )
return h return h

View File

@ -346,8 +346,8 @@ def get_address_from_input_script(bytes):
redeemScript = decoded[-1][1] redeemScript = decoded[-1][1]
num = len(match) - 2 num = len(match) - 2
signatures = map(lambda x:x[1].encode('hex'), decoded[1:-1]) signatures = map(lambda x:x[1][:-1].encode('hex'), decoded[1:-1])
dec2 = [ x for x in script_GetOp(redeemScript) ] dec2 = [ x for x in script_GetOp(redeemScript) ]
# 2 of 2 # 2 of 2

View File

@ -74,7 +74,7 @@ class Wallet:
self.seed_version = config.get('seed_version', SEED_VERSION) self.seed_version = config.get('seed_version', SEED_VERSION)
self.gap_limit = config.get('gap_limit', 5) self.gap_limit = config.get('gap_limit', 5)
self.use_change = config.get('use_change',True) self.use_change = config.get('use_change',True)
self.fee = int(config.get('fee_per_kb',50000)) self.fee = int(config.get('fee_per_kb',20000))
self.num_zeros = int(config.get('num_zeros',0)) self.num_zeros = int(config.get('num_zeros',0))
self.use_encryption = config.get('use_encryption', False) self.use_encryption = config.get('use_encryption', False)
self.seed = config.get('seed', '') # encrypted self.seed = config.get('seed', '') # encrypted
@ -172,62 +172,112 @@ class Wallet:
master_k, master_c, master_K, master_cK = bip32_init(self.seed) master_k, master_c, master_K, master_cK = bip32_init(self.seed)
# normal accounts
k0, c0, K0, cK0 = bip32_private_derivation(master_k, master_c, "m/", "m/0'/") k0, c0, K0, cK0 = bip32_private_derivation(master_k, master_c, "m/", "m/0'/")
# p2sh 2of2
k1, c1, K1, cK1 = bip32_private_derivation(master_k, master_c, "m/", "m/1'/") k1, c1, K1, cK1 = bip32_private_derivation(master_k, master_c, "m/", "m/1'/")
k2, c2, K2, cK2 = bip32_private_derivation(master_k, master_c, "m/", "m/2'/") k2, c2, K2, cK2 = bip32_private_derivation(master_k, master_c, "m/", "m/2'/")
# p2sh 2of3
k3, c3, K3, cK3 = bip32_private_derivation(master_k, master_c, "m/", "m/3'/")
k4, c4, K4, cK4 = bip32_private_derivation(master_k, master_c, "m/", "m/4'/")
k5, c5, K5, cK5 = bip32_private_derivation(master_k, master_c, "m/", "m/5'/")
self.master_public_keys = { self.master_public_keys = {
"m/0'/": (c0, K0, cK0), "m/0'/": (c0, K0, cK0),
"m/1'/": (c1, K1, cK1), "m/1'/": (c1, K1, cK1),
"m/2'/": (c2, K2, cK2) "m/2'/": (c2, K2, cK2),
"m/3'/": (c3, K3, cK3),
"m/4'/": (c4, K4, cK4),
"m/5'/": (c5, K5, cK5)
} }
self.master_private_keys = { self.master_private_keys = {
"m/0'/": k0, "m/0'/": k0,
"m/1'/": k1 "m/1'/": k1,
"m/2'/": k2,
"m/3'/": k3,
"m/4'/": k4,
"m/5'/": k5
} }
# send k2 to service
self.config.set_key('master_public_keys', self.master_public_keys, True) self.config.set_key('master_public_keys', self.master_public_keys, True)
self.config.set_key('master_private_keys', self.master_private_keys, True) self.config.set_key('master_private_keys', self.master_private_keys, True)
# create default account # create default account
self.create_new_account('Main account', None) self.create_account('Main account')
def create_new_account(self, name, password): def find_root_by_master_key(self, c, K):
keys = self.accounts.keys() for key, v in self.master_public_keys.items():
i = 0 if key == "m/":continue
cc, KK, _ = v
if (c == cc) and (K == KK):
return key
while True: def deseed_root(self, seed, password):
derivation = "m/0'/%d'"%i # for safety, we ask the user to enter their seed
if derivation not in keys: break assert seed == self.decode_seed(password)
i += 1 self.seed = ''
self.config.set_key('seed', '', True)
start = "m/0'/"
master_k = self.get_master_private_key(start, password )
master_c, master_K, master_cK = self.master_public_keys[start]
k, c, K, cK = bip32_private_derivation(master_k, master_c, start, derivation)
self.accounts[derivation] = BIP32_Account({ 'name':name, 'c':c, 'K':K, 'cK':cK })
self.save_accounts()
def create_p2sh_account(self, name): def deseed_branch(self, k):
# check that parent has no seed
assert self.seed == ''
self.master_private_keys.pop(k)
self.config.set_key('master_private_keys', self.master_private_keys, True)
def account_id(self, account_type, i):
if account_type is None:
return "m/0'/%d"%i
elif account_type == '2of2':
return "m/1'/%d & m/2'/%d"%(i,i)
elif account_type == '2of3':
return "m/3'/%d & m/4'/%d & m/5'/%d"%(i,i,i)
else:
raise BaseException('unknown account type')
def num_accounts(self, account_type):
keys = self.accounts.keys() keys = self.accounts.keys()
i = 0 i = 0
while True: while True:
account_id = "m/1'/%d & m/2'/%d"%(i,i) account_id = self.account_id(account_type, i)
if account_id not in keys: break if account_id not in keys: break
i += 1 i += 1
return i
master_c1, master_K1, _ = self.master_public_keys["m/1'/"]
c1, K1, cK1 = bip32_public_derivation(master_c1.decode('hex'), master_K1.decode('hex'), "m/1'/", "m/1'/%d"%i) def create_account(self, name, account_type = None):
i = self.num_accounts(account_type)
master_c2, master_K2, _ = self.master_public_keys["m/2'/"] account_id = self.account_id(account_type,i)
c2, K2, cK2 = bip32_public_derivation(master_c2.decode('hex'), master_K2.decode('hex'), "m/2'/", "m/2'/%d"%i)
if account_type is None:
self.accounts[account_id] = BIP32_Account_2of2({ 'name':name, 'c':c1, 'K':K1, 'cK':cK1, 'c2':c2, 'K2':K2, 'cK2':cK2 }) master_c0, master_K0, _ = self.master_public_keys["m/0'/"]
c0, K0, cK0 = bip32_public_derivation(master_c0.decode('hex'), master_K0.decode('hex'), "m/0'/", "m/0'/%d"%i)
account = BIP32_Account({ 'c':c0, 'K':K0, 'cK':cK0 })
elif account_type == '2of2':
master_c1, master_K1, _ = self.master_public_keys["m/1'/"]
c1, K1, cK1 = bip32_public_derivation(master_c1.decode('hex'), master_K1.decode('hex'), "m/1'/", "m/1'/%d"%i)
master_c2, master_K2, _ = self.master_public_keys["m/2'/"]
c2, K2, cK2 = bip32_public_derivation(master_c2.decode('hex'), master_K2.decode('hex'), "m/2'/", "m/2'/%d"%i)
account = BIP32_Account_2of2({ 'c':c1, 'K':K1, 'cK':cK1, 'c2':c2, 'K2':K2, 'cK2':cK2 })
elif account_type == '2of3':
master_c3, master_K3, _ = self.master_public_keys["m/3'/"]
c3, K3, cK3 = bip32_public_derivation(master_c3.decode('hex'), master_K3.decode('hex'), "m/3'/", "m/3'/%d"%i)
master_c4, master_K4, _ = self.master_public_keys["m/4'/"]
c4, K4, cK4 = bip32_public_derivation(master_c4.decode('hex'), master_K4.decode('hex'), "m/4'/", "m/4'/%d"%i)
master_c5, master_K5, _ = self.master_public_keys["m/5'/"]
c5, K5, cK5 = bip32_public_derivation(master_c5.decode('hex'), master_K5.decode('hex'), "m/5'/", "m/5'/%d"%i)
account = BIP32_Account_2of3({ 'c':c3, 'K':K3, 'cK':cK3, 'c2':c4, 'K2':K4, 'cK2':cK4, 'c3':c5, 'K3':K5, 'cK3':cK5 })
self.accounts[account_id] = account
self.save_accounts() self.save_accounts()
self.labels[account_id] = name
self.config.set_key('labels', self.labels, True)
def save_accounts(self): def save_accounts(self):
@ -283,15 +333,39 @@ class Wallet:
def get_address_index(self, address): def get_address_index(self, address):
if address in self.imported_keys.keys(): if address in self.imported_keys.keys():
return -1, None return -1, None
for account in self.accounts.keys(): for account in self.accounts.keys():
for for_change in [0,1]: for for_change in [0,1]:
addresses = self.accounts[account].get_addresses(for_change) addresses = self.accounts[account].get_addresses(for_change)
for addr in addresses: for addr in addresses:
if address == addr: if address == addr:
return account, (for_change, addresses.index(addr)) return account, (for_change, addresses.index(addr))
raise BaseException("not found") raise BaseException("not found")
def rebase_sequence(self, account, sequence):
c, i = sequence
dd = []
for a in account.split('&'):
s = a.strip()
m = re.match("(m/\d+'/)(\d+)", s)
root = m.group(1)
num = int(m.group(2))
dd.append( (root, [num,c,i] ) )
return dd
def get_keyID(self, account, sequence):
rs = self.rebase_sequence(account, sequence)
dd = []
for root, public_sequence in rs:
c, K, _ = self.master_public_keys[root]
s = '/' + '/'.join( map(lambda x:str(x), public_sequence) )
dd.append( 'bip32(%s,%s,%s)'%(c,K, s) )
return '&'.join(dd)
def get_public_key(self, address): def get_public_key(self, address):
account, sequence = self.get_address_index(address) account, sequence = self.get_address_index(address)
return self.accounts[account].get_pubkey( *sequence ) return self.accounts[account].get_pubkey( *sequence )
@ -304,50 +378,37 @@ class Wallet:
def get_private_key(self, address, password): def get_private_key(self, address, password):
out = []
if address in self.imported_keys.keys(): if address in self.imported_keys.keys():
return pw_decode( self.imported_keys[address], password ) out.append( pw_decode( self.imported_keys[address], password ) )
else: else:
account, sequence = self.get_address_index(address) account, sequence = self.get_address_index(address)
m = re.match("m/0'/(\d+)'", account) # assert address == self.accounts[account].get_address(*sequence)
if m: rs = self.rebase_sequence( account, sequence)
num = int(m.group(1)) for root, public_sequence in rs:
master_k = self.get_master_private_key("m/0'/", password)
master_c, _, _ = self.master_public_keys["m/0'/"]
master_k, master_c = CKD(master_k, master_c, num + BIP32_PRIME)
return self.accounts[account].get_private_key(sequence, master_k)
m2 = re.match("m/1'/(\d+) & m/2'/(\d+)", account)
if m2:
num = int(m2.group(1))
master_k = self.get_master_private_key("m/1'/", password)
master_c, master_K, _ = self.master_public_keys["m/1'/"]
master_k, master_c = CKD(master_k.decode('hex'), master_c.decode('hex'), num)
return self.accounts[account].get_private_key(sequence, master_k)
return
def get_private_keys(self, addresses, password):
if not self.seed: return {}
# decode seed in any case, in order to test the password
seed = self.decode_seed(password)
out = {}
for address in addresses:
pk = self.get_private_key(address, password)
if pk: out[address] = pk
if root not in self.master_private_keys.keys(): continue
master_k = self.get_master_private_key(root, password)
master_c, _, _ = self.master_public_keys[root]
pk = bip32_private_key( public_sequence, master_k.decode('hex'), master_c.decode('hex'))
out.append(pk)
return out return out
def signrawtransaction(self, tx, input_info, private_keys, password): def signrawtransaction(self, tx, input_info, private_keys, password):
import deserialize
unspent_coins = self.get_unspent_coins() unspent_coins = self.get_unspent_coins()
seed = self.decode_seed(password) seed = self.decode_seed(password)
# convert private_keys to dict # build a list of public/private keys
pk = {} keypairs = {}
for sec in private_keys: for sec in private_keys:
address = address_from_private_key(sec) pubkey = public_key_from_private_key(sec)
pk[address] = sec keypairs[ pubkey ] = sec
private_keys = pk
for txin in tx.inputs: for txin in tx.inputs:
# convert to own format # convert to own format
@ -363,33 +424,61 @@ class Wallet:
else: else:
for item in unspent_coins: for item in unspent_coins:
if txin['tx_hash'] == item['tx_hash'] and txin['index'] == item['index']: if txin['tx_hash'] == item['tx_hash'] and txin['index'] == item['index']:
print_error( "tx input is in unspent coins" )
txin['raw_output_script'] = item['raw_output_script'] txin['raw_output_script'] = item['raw_output_script']
account, sequence = self.get_address_index(item['address'])
if account != -1:
txin['redeemScript'] = self.accounts[account].redeem_script(sequence)
break break
else: else:
# if neither, we might want to get it from the server.. raise BaseException("Unknown transaction input. Please provide the 'input_info' parameter, or synchronize this wallet")
raise
# find the address: # if available, derive private_keys from KeyID
if txin.get('KeyID'): keyid = txin.get('KeyID')
account, name, sequence = txin.get('KeyID') if keyid:
if name != 'Electrum': continue roots = []
sec = self.accounts[account].get_private_key(sequence, seed) for s in keyid.split('&'):
addr = self.accounts[account].get_address(sequence) m = re.match("bip32\(([0-9a-f]+),([0-9a-f]+),(/\d+/\d+/\d+)", s)
if not m: continue
c = m.group(1)
K = m.group(2)
sequence = m.group(3)
root = self.find_root_by_master_key(c,K)
if not root: continue
sequence = map(lambda x:int(x), sequence.strip('/').split('/'))
root = root + '%d'%sequence[0]
sequence = sequence[1:]
roots.append((root,sequence))
account_id = " & ".join( map(lambda x:x[0], roots) )
account = self.accounts.get(account_id)
if not account: continue
addr = account.get_address(*sequence)
txin['address'] = addr txin['address'] = addr
private_keys[addr] = sec pk = self.get_private_key(addr, password)
for sec in pk:
pubkey = public_key_from_private_key(sec)
keypairs[pubkey] = sec
elif txin.get("redeemScript"): redeem_script = txin.get("redeemScript")
txin['address'] = hash_160_to_bc_address(hash_160(txin.get("redeemScript").decode('hex')), 5) print_error( "p2sh:", "yes" if redeem_script else "no")
if redeem_script:
addr = hash_160_to_bc_address(hash_160(redeem_script.decode('hex')), 5)
else:
addr = deserialize.get_address_from_output_script(txin["raw_output_script"].decode('hex'))
txin['address'] = addr
elif txin.get("raw_output_script"): # add private keys that are in the wallet
import deserialize pk = self.get_private_key(addr, password)
addr = deserialize.get_address_from_output_script(txin.get("raw_output_script").decode('hex')) for sec in pk:
sec = self.get_private_key(addr, password) pubkey = public_key_from_private_key(sec)
if sec: keypairs[pubkey] = sec
private_keys[addr] = sec if not redeem_script:
txin['address'] = addr txin['redeemPubkey'] = pubkey
tx.sign( private_keys ) print txin
tx.sign( keypairs )
def sign_message(self, address, message, password): def sign_message(self, address, message, password):
sec = self.get_private_key(address, password) sec = self.get_private_key(address, password)
@ -513,7 +602,7 @@ class Wallet:
self.config.set_key('contacts', self.addressbook, True) self.config.set_key('contacts', self.addressbook, True)
if label: if label:
self.labels[address] = label self.labels[address] = label
self.config.set_key('labels', self.labels) self.config.set_key('labels', self.labels, True)
def delete_contact(self, addr): def delete_contact(self, addr):
if addr in self.addressbook: if addr in self.addressbook:
@ -606,7 +695,7 @@ class Wallet:
def get_accounts(self): def get_accounts(self):
accounts = {} accounts = {}
for k, account in self.accounts.items(): for k, account in self.accounts.items():
accounts[k] = account.name accounts[k] = self.labels.get(k, 'unnamed')
if self.imported_keys: if self.imported_keys:
accounts[-1] = 'Imported keys' accounts[-1] = 'Imported keys'
return accounts return accounts
@ -873,13 +962,6 @@ class Wallet:
def mktx(self, outputs, password, fee=None, change_addr=None, account=None ): def mktx(self, outputs, password, fee=None, change_addr=None, account=None ):
"""
create a transaction
account parameter:
None means use all accounts
-1 means imported keys
0, 1, etc are seed accounts
"""
for address, x in outputs: for address, x in outputs:
assert is_valid(address) assert is_valid(address)
@ -891,33 +973,28 @@ class Wallet:
raise ValueError("Not enough funds") raise ValueError("Not enough funds")
outputs = self.add_tx_change(inputs, outputs, amount, fee, total, change_addr, account) outputs = self.add_tx_change(inputs, outputs, amount, fee, total, change_addr, account)
tx = Transaction.from_io(inputs, outputs) tx = Transaction.from_io(inputs, outputs)
pk_addresses = [] keypairs = {}
for i in range(len(tx.inputs)): for i, txin in enumerate(tx.inputs):
txin = tx.inputs[i]
address = txin['address'] address = txin['address']
if address in self.imported_keys.keys():
pk_addresses.append(address)
continue
account, sequence = self.get_address_index(address) account, sequence = self.get_address_index(address)
txin['KeyID'] = self.get_keyID(account, sequence)
txin['KeyID'] = (account, 'BIP32', sequence) # used by the server to find the key redeemScript = self.accounts[account].redeem_script(sequence)
if redeemScript:
txin['redeemScript'] = redeemScript
else:
txin['redeemPubkey'] = self.accounts[account].get_pubkey(*sequence)
_, redeemScript = self.accounts[account].get_input_info(sequence) private_keys = self.get_private_key(address, password)
if redeemScript: txin['redeemScript'] = redeemScript
pk_addresses.append(address)
print "pk_addresses", pk_addresses for sec in private_keys:
pubkey = public_key_from_private_key(sec)
# get all private keys at once. keypairs[ pubkey ] = sec
if self.seed:
private_keys = self.get_private_keys(pk_addresses, password)
print "private keys", private_keys
tx.sign(private_keys)
tx.sign(keypairs)
for address, x in outputs: for address, x in outputs:
if address not in self.addressbook and not self.is_mine(address): if address not in self.addressbook and not self.is_mine(address):
self.addressbook.append(address) self.addressbook.append(address)