electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

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:
Melectrum | 95++++++++++++++++++-------------------------------------------------------------
Mgui/gui_classic.py | 302+++++++++++++++++--------------------------------------------------------------
Agui/installwizard.py | 183+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/password_dialog.py | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mgui/plugins.py | 1+
Agui/seed_dialog.py | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/account.py | 68+++++++++++++++++++++++++++++++++++++-------------------------------
Mlib/bitcoin.py | 142++++++++++++++++++++++++++++++++-----------------------------------------------
Mlib/commands.py | 44++++++++++++++++++++++++++++++++------------
Mlib/deserialize.py | 4++--
Mlib/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)