Browse Source

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

283
thomasv 12 years ago
parent
commit
238ed35134
  1. 93
      electrum
  2. 300
      gui/gui_classic.py
  3. 183
      gui/installwizard.py
  4. 104
      gui/password_dialog.py
  5. 1
      gui/plugins.py
  6. 82
      gui/seed_dialog.py
  7. 68
      lib/account.py
  8. 112
      lib/bitcoin.py
  9. 34
      lib/commands.py
  10. 2
      lib/deserialize.py
  11. 281
      lib/wallet.py

93
electrum

@ -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 )
interface = Interface(config, True)
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) traceback.print_exc(file=sys.stdout)
exit() sys.exit()
#sys.exit("Error: Unknown GUI: " + gui_name )
if not keep_it: exit()
if not found:
gui.password_dialog()
#wallet.save() gui = gui.ElectrumGui(config)
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'
@ -338,6 +274,16 @@ if __name__ == '__main__':
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:
parser.print_help() parser.print_help()
@ -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:

300
gui/gui_classic.py

@ -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:
@ -1407,6 +1423,13 @@ class ElectrumWindow(QMainWindow):
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
self.config.set_key('gui', 'lite', True) self.config.set_key('gui', 'lite', True)
@ -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)
@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")+ ":") from seed_dialog import SeedDialog
d = SeedDialog(self)
seed_text = QTextEdit(brainwallet) d.show_seed(seed, self.wallet.imported_keys)
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 verify_seed(self):
r = self.seed_dialog(False)
if r != self.wallet.seed:
QMessageBox.warning(None, _('Error'), 'incorrect seed', 'OK')
return False
else:
return True
def seed_dialog(self, is_restore=True): def main(self, url):
d = QDialog()
d.setModal(1)
vbox = QVBoxLayout() found = self.config.wallet_file_exists
if is_restore: if not found:
msg = _("Please enter your wallet seed (or your master public key if you want to create a watching-only wallet)." + ' ') import installwizard
wizard = installwizard.InstallWizard(self.config, self.interface)
wallet = wizard.run()
if not wallet:
exit()
else: else:
msg = _("Your seed is important! To make sure that you have properly saved your seed, please type it here." + ' ') wallet = Wallet(self.config)
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() self.wallet = wallet
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"\ self.interface.start(wait = False)
%(_("Please wait..."),_("Addresses generated:"),len(wallet.addresses(True)),_("Kilobytes received:"), wallet.interface.bytes_received/1024.) self.interface.send([('server.peers.subscribe',[])])
wallet.interface = self.interface
wallet.set_up_to_date(False) verifier = WalletVerifier(self.interface, self.config)
wallet.interface.poke('synchronizer') verifier.start()
waiting_dialog(waiting) wallet.set_verifier(verifier)
if wallet.is_found(): synchronizer = WalletSynchronizer(wallet, self.config)
print_error( "Recovery successful" ) synchronizer.start()
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

@ -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

@ -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 )

1
gui/plugins.py

@ -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

@ -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_()

68
lib/account.py

@ -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 redeem_script(self, sequence):
chain, i = sequence
pubkey1 = self.get_pubkey(chain, i)
pubkey2 = self.get_pubkey2(chain, i)
return Transaction.multisig_script([pubkey1, pubkey2], 2)
def get_address(self, for_change, n): def get_address(self, for_change, n):
pubkey1 = self.get_pubkey(for_change, n) address = hash_160_to_bc_address(hash_160(self.redeem_script((for_change, n)).decode('hex')), 5)
pubkey2 = self.get_pubkey2(for_change, n)
address = Transaction.multisig_script([pubkey1, pubkey2], 2)["address"]
return address return address
def get_input_info(self, sequence):
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 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 pubkey3 = self.get_pubkey3(chain, i)
pk_addr = None # public_key_to_bc_address( pubkey1 ) # we need to return that address to get the right private key return Transaction.multisig_script([pubkey1, pubkey2, pubkey3], 3)
redeemScript = Transaction.multisig_script([pubkey1, pubkey2], 2)['redeemScript']
return pk_addr, redeemScript

112
lib/bitcoin.py

@ -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')
if pubkeysig:
pubkey, sig = pubkeysig[0]
sig = sig + chr(1) # hashtype
script = op_push( len(sig))
script += sig.encode('hex')
script += op_push( len(pubkey))
script += pubkey.encode('hex')
else:
signatures = txin['signatures'] signatures = txin['signatures']
pubkeys = txin['pubkeys'] pubkeys = txin['pubkeys']
if not txin.get('redeemScript'):
pubkey = pubkeys[0]
sig = signatures[0]
sig = sig + '01' # hashtype
script = op_push(len(sig)/2)
script += sig
script += op_push(len(pubkey)/2)
script += pubkey
else:
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,47 +591,30 @@ 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
keypairs = {}
for sec in private_keys.values():
compressed = is_compressed(sec)
pkey = regenerate_key(sec)
pubkey = GetPubKey(pkey.pubkey, compressed)
keypairs[ pubkey.encode('hex') ] = sec
print "keypairs", keypairs
print redeem_script, redeem_pubkeys
# list of already existing signatures # add pubkeys
txin["pubkeys"] = redeem_pubkeys
# get list of already existing signatures
signatures = txin.get("signatures",[]) signatures = txin.get("signatures",[])
print_error("signatures",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: for pubkey in redeem_pubkeys:
# check if we have the corresponding private key
# 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(): if pubkey in keypairs.keys():
# add signature # add signature
sec = keypairs[pubkey] sec = keypairs[pubkey]
@ -639,27 +626,12 @@ class Transaction:
sig = private_key.sign_digest( Hash( tx_for_sig.decode('hex') ), sigencode = ecdsa.util.sigencode_der ) 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) assert public_key.verify_digest( sig, Hash( tx_for_sig.decode('hex') ), sigdecode = ecdsa.util.sigdecode_der)
signatures.append( sig.encode('hex') ) signatures.append( sig.encode('hex') )
print_error("adding signature for", pubkey)
# for p2sh, pubkeysig is a tuple (may be incomplete) txin["signatures"] = signatures
self.inputs[i]["signatures"] = signatures is_complete = is_complete and len(signatures) == num
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 )

34
lib/commands.py

@ -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,8 +209,9 @@ 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):
for to_address, amount in outputs:
if not is_valid(to_address): if not is_valid(to_address):
raise BaseException("Invalid Bitcoin address", to_address) raise BaseException("Invalid Bitcoin address", to_address)
@ -222,26 +227,41 @@ class Commands:
if not self.wallet.is_mine(addr): if not self.wallet.is_mine(addr):
raise BaseException("address not in wallet", addr) raise BaseException("address not in wallet", addr)
for k, v in self.wallet.labels.items():
if change_addr and v == change_addr:
change_addr = k
final_outputs = []
for to_address, amount in outputs:
for k, v in self.wallet.labels.items(): for k, v in self.wallet.labels.items():
if v == to_address: if v == to_address:
to_address = k to_address = k
print_msg("alias", to_address) print_msg("alias", to_address)
break break
if change_addr and v == change_addr:
change_addr = k
amount = int(100000000*amount) 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

2
lib/deserialize.py

@ -346,7 +346,7 @@ 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) ]

281
lib/wallet.py

@ -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 }) def deseed_branch(self, k):
self.save_accounts() # 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 create_p2sh_account(self, name):
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
def create_account(self, name, account_type = None):
i = self.num_accounts(account_type)
account_id = self.account_id(account_type,i)
if account_type is None:
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'/"] 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) 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'/"] 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) 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 })
self.accounts[account_id] = BIP32_Account_2of2({ 'name':name, '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
if root not in self.master_private_keys.keys(): continue
def get_private_keys(self, addresses, password): master_k = self.get_master_private_key(root, password)
if not self.seed: return {} master_c, _, _ = self.master_public_keys[root]
# decode seed in any case, in order to test the password pk = bip32_private_key( public_sequence, master_k.decode('hex'), master_c.decode('hex'))
seed = self.decode_seed(password) out.append(pk)
out = {}
for address in addresses:
pk = self.get_private_key(address, password)
if pk: out[address] = 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
# if available, derive private_keys from KeyID
# find the address: keyid = txin.get('KeyID')
if txin.get('KeyID'): if keyid:
account, name, sequence = txin.get('KeyID') roots = []
if name != 'Electrum': continue for s in keyid.split('&'):
sec = self.accounts[account].get_private_key(sequence, seed) m = re.match("bip32\(([0-9a-f]+),([0-9a-f]+),(/\d+/\d+/\d+)", s)
addr = self.accounts[account].get_address(sequence) 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
pk = self.get_private_key(addr, password)
for sec in pk:
pubkey = public_key_from_private_key(sec)
keypairs[pubkey] = sec
redeem_script = txin.get("redeemScript")
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 txin['address'] = addr
private_keys[addr] = sec
elif txin.get("redeemScript"): # add private keys that are in the wallet
txin['address'] = hash_160_to_bc_address(hash_160(txin.get("redeemScript").decode('hex')), 5) pk = self.get_private_key(addr, password)
for sec in pk:
pubkey = public_key_from_private_key(sec)
keypairs[pubkey] = sec
if not redeem_script:
txin['redeemPubkey'] = pubkey
elif txin.get("raw_output_script"): print txin
import deserialize
addr = deserialize.get_address_from_output_script(txin.get("raw_output_script").decode('hex'))
sec = self.get_private_key(addr, password)
if sec:
private_keys[addr] = sec
txin['address'] = addr
tx.sign( private_keys ) 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)
txin['KeyID'] = (account, 'BIP32', sequence) # used by the server to find the key
_, redeemScript = self.accounts[account].get_input_info(sequence) account, sequence = self.get_address_index(address)
txin['KeyID'] = self.get_keyID(account, sequence)
if redeemScript: txin['redeemScript'] = redeemScript redeemScript = self.accounts[account].redeem_script(sequence)
pk_addresses.append(address) if redeemScript:
txin['redeemScript'] = redeemScript
else:
txin['redeemPubkey'] = self.accounts[account].get_pubkey(*sequence)
print "pk_addresses", pk_addresses private_keys = self.get_private_key(address, password)
# get all private keys at once. for sec in private_keys:
if self.seed: pubkey = public_key_from_private_key(sec)
private_keys = self.get_private_keys(pk_addresses, password) keypairs[ pubkey ] = sec
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)

Loading…
Cancel
Save