commit 238ed351349eb1a84cba0b4c940c72e5e6cd98c8
parent f4c26dfac0a8b518aeee4adb5215971b72209c39
Author: thomasv <thomasv@gitorious>
Date: Tue, 27 Aug 2013 13:59:20 +0200
Merge branch '1.9' of git://github.com/spesmilo/electrum into 1.9
Diffstat:
M | electrum | | | 95 | ++++++++++++++++++------------------------------------------------------------- |
M | gui/gui_classic.py | | | 302 | +++++++++++++++++-------------------------------------------------------------- |
A | gui/installwizard.py | | | 183 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | gui/password_dialog.py | | | 104 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | gui/plugins.py | | | 1 | + |
A | gui/seed_dialog.py | | | 82 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | lib/account.py | | | 68 | +++++++++++++++++++++++++++++++++++++------------------------------- |
M | lib/bitcoin.py | | | 142 | ++++++++++++++++++++++++++++++++----------------------------------------------- |
M | lib/commands.py | | | 44 | ++++++++++++++++++++++++++++++++------------ |
M | lib/deserialize.py | | | 4 | ++-- |
M | lib/wallet.py | | | 297 | ++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------- |
11 files changed, 768 insertions(+), 554 deletions(-)
diff --git a/electrum b/electrum
@@ -22,6 +22,7 @@ import sys, os, time, json
import optparse
import platform
from decimal import Decimal
+import traceback
try:
import ecdsa
@@ -106,7 +107,6 @@ if __name__ == '__main__':
util.check_windows_wallet_migration()
config = SimpleConfig(config_options)
- wallet = Wallet(config)
if len(args)==0:
@@ -124,86 +124,22 @@ if __name__ == '__main__':
try:
gui = __import__('electrum_gui.gui_' + gui_name, fromlist=['electrum_gui'])
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)
- 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 = gui.ElectrumGui(config)
gui.main(url)
- #wallet.save()
-
- verifier.stop()
- synchronizer.stop()
- interface.stop()
# we use daemon threads, their termination is enforced.
# this sleep command gives them time to terminate cleanly.
time.sleep(0.1)
sys.exit(0)
+
+ # instanciate wallet for command-line
+ wallet = Wallet(config)
+
if cmd not in known_commands:
cmd = 'help'
@@ -337,6 +273,16 @@ if __name__ == '__main__':
elif cmd in ['payto', 'mktx']:
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 ]
+
+ 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':
if len(args) < 2:
@@ -429,7 +375,8 @@ if __name__ == '__main__':
try:
result = func(*args[1:])
except BaseException, e:
- print_msg("Error: " + str(e))
+ import traceback
+ traceback.print_exc(file=sys.stdout)
sys.exit(1)
if type(result) == str:
diff --git a/gui/gui_classic.py b/gui/gui_classic.py
@@ -42,7 +42,7 @@ except:
from electrum.wallet import format_satoshis
from electrum.bitcoin import Transaction, is_valid
from electrum import mnemonic
-from electrum import util, bitcoin, commands
+from electrum import util, bitcoin, commands, Interface, Wallet, WalletVerifier, WalletSynchronizer
import bmp, pyqrnative
import exchange_rate
@@ -265,6 +265,7 @@ class ElectrumWindow(QMainWindow):
if reason == QSystemTrayIcon.DoubleClick:
self.showNormal()
+
def __init__(self, wallet, config):
QMainWindow.__init__(self)
self._close_electrum = False
@@ -352,10 +353,21 @@ class ElectrumWindow(QMainWindow):
wallet_folder = self.wallet.config.path
re.sub("(\/\w*.dat)$", "", wallet_folder)
file_name = QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder, "*.dat")
- if not file_name:
- return
- else:
- self.load_wallet(file_name)
+ return file_name
+
+ def open_wallet(self):
+ 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):
@@ -363,7 +375,10 @@ class ElectrumWindow(QMainWindow):
electrum_menu = menubar.addMenu(_("&File"))
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")
if sys.platform == 'darwin':
@@ -430,6 +445,7 @@ class ElectrumWindow(QMainWindow):
self.setMenuBar(menubar)
+
def load_wallet(self, filename):
import electrum
@@ -1268,7 +1284,7 @@ class ElectrumWindow(QMainWindow):
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)
account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
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 ) )
if self.wallet.seed:
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( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
if self.wallet.seed:
@@ -1406,6 +1422,13 @@ class ElectrumWindow(QMainWindow):
self.run_hook('create_status_bar', (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):
import gui_lite
@@ -1490,63 +1513,12 @@ class ElectrumWindow(QMainWindow):
except:
QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
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")+ ":")
-
- 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)
+ from seed_dialog import SeedDialog
+ d = SeedDialog(self)
+ d.show_seed(seed, self.wallet.imported_keys)
- 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"):
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):
tabs = QTabWidget(self)
@@ -2282,10 +2181,13 @@ class OpenFileEventFilter(QObject):
return True
return False
+
+
+
class ElectrumGui:
- def __init__(self, wallet, config, app=None):
- self.wallet = wallet
+ def __init__(self, config, app=None):
+ self.interface = Interface(config, True)
self.config = config
self.windows = []
self.efilter = OpenFileEventFilter(self.windows)
@@ -2293,116 +2195,32 @@ class ElectrumGui:
self.app = QApplication(sys.argv)
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):
- 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
+ def main(self, url):
+
+ found = self.config.wallet_file_exists
+ if not found:
+ import installwizard
+ wizard = installwizard.InstallWizard(self.config, self.interface)
+ wallet = wizard.run()
+ if not wallet:
+ exit()
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)
+ wallet = Wallet(self.config)
- 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)
+ self.wallet = wallet
- 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.)
+ self.interface.start(wait = False)
+ self.interface.send([('server.peers.subscribe',[])])
+ wallet.interface = self.interface
- 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'))
+ verifier = WalletVerifier(self.interface, self.config)
+ verifier.start()
+ wallet.set_verifier(verifier)
+ synchronizer = WalletSynchronizer(wallet, self.config)
+ synchronizer.start()
- return True
- def main(self,url):
s = Timer()
s.start()
w = ElectrumWindow(self.wallet, self.config)
@@ -2415,4 +2233,8 @@ class ElectrumGui:
self.app.exec_()
+ verifier.stop()
+ synchronizer.stop()
+ self.interface.stop()
+
diff --git a/gui/installwizard.py b/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)
diff --git a/gui/password_dialog.py b/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 )
+
+
+
diff --git a/gui/plugins.py b/gui/plugins.py
@@ -4,6 +4,7 @@ class BasePlugin:
def __init__(self, gui, name):
self.gui = gui
+ self.wallet = self.gui.wallet
self.name = name
self.config = gui.config
diff --git a/gui/seed_dialog.py b/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_()
diff --git a/lib/account.py b/lib/account.py
@@ -24,13 +24,9 @@ class Account(object):
def __init__(self, v):
self.addresses = v.get('0', [])
self.change = v.get('1', [])
- self.name = v.get('name', 'unnamed')
def dump(self):
- return {'0':self.addresses, '1':self.change, 'name':self.name}
-
- def get_name(self):
- return self.name
+ return {'0':self.addresses, '1':self.change}
def get_addresses(self, for_change):
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)
return K_compressed.encode('hex')
- def get_private_key(self, sequence, master_k):
- chain = self.c
- 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 redeem_script(self, sequence):
+ return None
- 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)
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):
- pubkey1 = self.get_pubkey(for_change, n)
- pubkey2 = self.get_pubkey2(for_change, n)
- address = Transaction.multisig_script([pubkey1, pubkey2], 2)["address"]
+ address = hash_160_to_bc_address(hash_160(self.redeem_script((for_change, n)).decode('hex')), 5)
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
pubkey1 = self.get_pubkey(chain, i)
pubkey2 = self.get_pubkey2(chain, i)
- # fixme
- 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']
- return pk_addr, redeemScript
+ pubkey3 = self.get_pubkey3(chain, i)
+ return Transaction.multisig_script([pubkey1, pubkey2, pubkey3], 3)
+
diff --git a/lib/bitcoin.py b/lib/bitcoin.py
@@ -244,17 +244,17 @@ def is_compressed(sec):
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
pkey = regenerate_key(sec)
assert pkey
-
- # figure out if private key is compressed
compressed = is_compressed(sec)
-
- # rebuild private and public key from regenerated secret
- private_key = GetPrivKey(pkey, 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)
return address
@@ -448,6 +448,11 @@ def bip32_public_derivation(c, K, branch, sequence):
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
s += 'ae'
- out = { "address": hash_160_to_bc_address(hash_160(s.decode('hex')), 5), "redeemScript":s }
- return out
+ return s
@classmethod
def serialize( klass, inputs, outputs, for_sig = None ):
@@ -522,24 +526,24 @@ class Transaction:
s += int_to_hex(txin['index'],4) # prev index
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')
+ signatures = txin['signatures']
+ 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:
- signatures = txin['signatures']
- pubkeys = txin['pubkeys']
script = '00' # op_0
for sig in signatures:
sig = sig + '01'
script += op_push(len(sig)/2)
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 += redeem_script
@@ -587,79 +591,47 @@ class Transaction:
def hash(self):
return Hash(self.raw.decode('hex') )[::-1].encode('hex')
- def sign(self, private_keys):
+
+
+ def sign(self, keypairs):
import deserialize
+ is_complete = True
+ print_error("tx.sign(), keypairs:", keypairs)
- for i in range(len(self.inputs)):
- txin = self.inputs[i]
- tx_for_sig = self.serialize( self.inputs, self.outputs, for_sig = i )
+ for i, txin in enumerate(self.inputs):
+ # if the input is multisig, parse redeem script
redeem_script = txin.get('redeemScript')
- if redeem_script:
- # 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():
+ num, redeem_pubkeys = deserialize.parse_redeemScript(redeem_script) if redeem_script else (1, [txin.get('redeemPubkey')])
+
+ # add pubkeys
+ txin["pubkeys"] = redeem_pubkeys
+ # 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)
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
- signatures = txin.get("signatures",[])
- print_error("signatures",signatures)
-
- 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
+ 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') )
+ print_error("adding signature for", pubkey)
+
+ txin["signatures"] = signatures
+ is_complete = is_complete and len(signatures) == num
+ self.is_complete = is_complete
self.raw = self.serialize( self.inputs, self.outputs )
diff --git a/lib/commands.py b/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('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('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('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('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)
@@ -131,7 +133,9 @@ class Commands:
def createmultisig(self, num, pubkeys):
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):
return self.wallet.freeze(addr)
@@ -205,10 +209,11 @@ class Commands:
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):
- raise BaseException("Invalid Bitcoin address", to_address)
+ for to_address, amount in outputs:
+ if not is_valid(to_address):
+ raise BaseException("Invalid Bitcoin address", to_address)
if change_addr:
if not is_valid(change_addr):
@@ -223,25 +228,40 @@ class Commands:
raise BaseException("address not in wallet", addr)
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:
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)
- 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):
- 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()
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 )
return h
diff --git a/lib/deserialize.py b/lib/deserialize.py
@@ -346,8 +346,8 @@ def get_address_from_input_script(bytes):
redeemScript = decoded[-1][1]
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) ]
# 2 of 2
diff --git a/lib/wallet.py b/lib/wallet.py
@@ -74,7 +74,7 @@ class Wallet:
self.seed_version = config.get('seed_version', SEED_VERSION)
self.gap_limit = config.get('gap_limit', 5)
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.use_encryption = config.get('use_encryption', False)
self.seed = config.get('seed', '') # encrypted
@@ -172,62 +172,112 @@ class Wallet:
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'/")
+ # p2sh 2of2
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'/")
+ # 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 = {
"m/0'/": (c0, K0, cK0),
"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 = {
"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_private_keys', self.master_private_keys, True)
# create default account
- self.create_new_account('Main account', None)
+ self.create_account('Main account')
- def create_new_account(self, name, password):
- keys = self.accounts.keys()
- i = 0
+ def find_root_by_master_key(self, c, K):
+ for key, v in self.master_public_keys.items():
+ if key == "m/":continue
+ cc, KK, _ = v
+ if (c == cc) and (K == KK):
+ return key
- while True:
- derivation = "m/0'/%d'"%i
- if derivation not in keys: break
- i += 1
+ def deseed_root(self, seed, password):
+ # for safety, we ask the user to enter their seed
+ assert seed == self.decode_seed(password)
+ self.seed = ''
+ self.config.set_key('seed', '', True)
+
+
+ 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)
- 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 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()
i = 0
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
i += 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)
-
- 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)
-
- self.accounts[account_id] = BIP32_Account_2of2({ 'name':name, 'c':c1, 'K':K1, 'cK':cK1, 'c2':c2, 'K2':K2, 'cK2':cK2 })
+ 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'/"]
+ 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.labels[account_id] = name
+ self.config.set_key('labels', self.labels, True)
def save_accounts(self):
@@ -283,15 +333,39 @@ class Wallet:
def get_address_index(self, address):
if address in self.imported_keys.keys():
return -1, None
+
for account in self.accounts.keys():
for for_change in [0,1]:
addresses = self.accounts[account].get_addresses(for_change)
for addr in addresses:
if address == addr:
return account, (for_change, addresses.index(addr))
+
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):
account, sequence = self.get_address_index(address)
return self.accounts[account].get_pubkey( *sequence )
@@ -304,50 +378,37 @@ class Wallet:
def get_private_key(self, address, password):
+ out = []
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:
account, sequence = self.get_address_index(address)
- m = re.match("m/0'/(\d+)'", account)
- if m:
- num = int(m.group(1))
- 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
-
+ # assert address == self.accounts[account].get_address(*sequence)
+ rs = self.rebase_sequence( account, sequence)
+ for root, public_sequence in rs:
+
+ 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
+
+
def signrawtransaction(self, tx, input_info, private_keys, password):
+ import deserialize
unspent_coins = self.get_unspent_coins()
seed = self.decode_seed(password)
- # convert private_keys to dict
- pk = {}
+ # build a list of public/private keys
+ keypairs = {}
for sec in private_keys:
- address = address_from_private_key(sec)
- pk[address] = sec
- private_keys = pk
+ pubkey = public_key_from_private_key(sec)
+ keypairs[ pubkey ] = sec
+
for txin in tx.inputs:
# convert to own format
@@ -363,33 +424,61 @@ class Wallet:
else:
for item in unspent_coins:
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']
+ account, sequence = self.get_address_index(item['address'])
+ if account != -1:
+ txin['redeemScript'] = self.accounts[account].redeem_script(sequence)
break
else:
- # if neither, we might want to get it from the server..
- raise
-
- # find the address:
- if txin.get('KeyID'):
- account, name, sequence = txin.get('KeyID')
- if name != 'Electrum': continue
- sec = self.accounts[account].get_private_key(sequence, seed)
- addr = self.accounts[account].get_address(sequence)
+ raise BaseException("Unknown transaction input. Please provide the 'input_info' parameter, or synchronize this wallet")
+
+ # if available, derive private_keys from KeyID
+ keyid = txin.get('KeyID')
+ if keyid:
+ roots = []
+ for s in keyid.split('&'):
+ 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
- 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
+
+ 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
- elif txin.get("redeemScript"):
- txin['address'] = hash_160_to_bc_address(hash_160(txin.get("redeemScript").decode('hex')), 5)
+ # add private keys that are in the wallet
+ 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"):
- 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
+ print txin
- tx.sign( private_keys )
+ tx.sign( keypairs )
def sign_message(self, address, message, password):
sec = self.get_private_key(address, password)
@@ -513,7 +602,7 @@ class Wallet:
self.config.set_key('contacts', self.addressbook, True)
if 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):
if addr in self.addressbook:
@@ -606,7 +695,7 @@ class Wallet:
def get_accounts(self):
accounts = {}
for k, account in self.accounts.items():
- accounts[k] = account.name
+ accounts[k] = self.labels.get(k, 'unnamed')
if self.imported_keys:
accounts[-1] = 'Imported keys'
return accounts
@@ -873,13 +962,6 @@ class Wallet:
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:
assert is_valid(address)
@@ -891,33 +973,28 @@ class Wallet:
raise ValueError("Not enough funds")
outputs = self.add_tx_change(inputs, outputs, amount, fee, total, change_addr, account)
-
tx = Transaction.from_io(inputs, outputs)
- pk_addresses = []
- for i in range(len(tx.inputs)):
- txin = tx.inputs[i]
+ keypairs = {}
+ for i, txin in enumerate(tx.inputs):
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
+ account, sequence = self.get_address_index(address)
+ txin['KeyID'] = self.get_keyID(account, sequence)
- _, redeemScript = self.accounts[account].get_input_info(sequence)
-
- if redeemScript: txin['redeemScript'] = redeemScript
- pk_addresses.append(address)
+ redeemScript = self.accounts[account].redeem_script(sequence)
+ 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.
- if self.seed:
- private_keys = self.get_private_keys(pk_addresses, password)
- print "private keys", private_keys
- tx.sign(private_keys)
+ for sec in private_keys:
+ pubkey = public_key_from_private_key(sec)
+ keypairs[ pubkey ] = sec
+ tx.sign(keypairs)
for address, x in outputs:
if address not in self.addressbook and not self.is_mine(address):
self.addressbook.append(address)