Merge branch '1.9' of git://github.com/spesmilo/electrum into 1.9
This commit is contained in:
commit
238ed35134
95
electrum
95
electrum
|
@ -22,6 +22,7 @@ import sys, os, time, json
|
||||||
import optparse
|
import optparse
|
||||||
import platform
|
import platform
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
import traceback
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import ecdsa
|
import ecdsa
|
||||||
|
@ -106,7 +107,6 @@ if __name__ == '__main__':
|
||||||
util.check_windows_wallet_migration()
|
util.check_windows_wallet_migration()
|
||||||
|
|
||||||
config = SimpleConfig(config_options)
|
config = SimpleConfig(config_options)
|
||||||
wallet = Wallet(config)
|
|
||||||
|
|
||||||
|
|
||||||
if len(args)==0:
|
if len(args)==0:
|
||||||
|
@ -124,86 +124,22 @@ if __name__ == '__main__':
|
||||||
try:
|
try:
|
||||||
gui = __import__('electrum_gui.gui_' + gui_name, fromlist=['electrum_gui'])
|
gui = __import__('electrum_gui.gui_' + gui_name, fromlist=['electrum_gui'])
|
||||||
except ImportError:
|
except ImportError:
|
||||||
sys.exit("Error: Unknown GUI: " + gui_name )
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
sys.exit()
|
||||||
|
#sys.exit("Error: Unknown GUI: " + gui_name )
|
||||||
|
|
||||||
interface = Interface(config, True)
|
gui = gui.ElectrumGui(config)
|
||||||
wallet.interface = interface
|
|
||||||
|
|
||||||
gui = gui.ElectrumGui(wallet, config)
|
|
||||||
|
|
||||||
found = config.wallet_file_exists
|
|
||||||
if not found:
|
|
||||||
a = gui.restore_or_create()
|
|
||||||
if not a: exit()
|
|
||||||
|
|
||||||
if a =='create':
|
|
||||||
wallet.init_seed(None)
|
|
||||||
gui.show_seed()
|
|
||||||
if gui.verify_seed():
|
|
||||||
wallet.save_seed()
|
|
||||||
else:
|
|
||||||
exit()
|
|
||||||
|
|
||||||
else:
|
|
||||||
# ask for seed and gap.
|
|
||||||
sg = gui.seed_dialog()
|
|
||||||
if not sg: exit()
|
|
||||||
seed, gap = sg
|
|
||||||
if not seed: exit()
|
|
||||||
wallet.gap_limit = gap
|
|
||||||
if len(seed) == 128:
|
|
||||||
wallet.seed = ''
|
|
||||||
wallet.init_sequence(str(seed))
|
|
||||||
else:
|
|
||||||
wallet.init_seed(str(seed))
|
|
||||||
wallet.save_seed()
|
|
||||||
|
|
||||||
# select a server.
|
|
||||||
s = gui.network_dialog()
|
|
||||||
if s is None:
|
|
||||||
config.set_key("server", None, True)
|
|
||||||
config.set_key('auto_cycle', False, True)
|
|
||||||
|
|
||||||
interface.start(wait = False)
|
|
||||||
interface.send([('server.peers.subscribe',[])])
|
|
||||||
|
|
||||||
# generate the first addresses, in case we are offline
|
|
||||||
if not found and ( s is None or a == 'create'):
|
|
||||||
wallet.synchronize()
|
|
||||||
|
|
||||||
verifier = WalletVerifier(interface, config)
|
|
||||||
verifier.start()
|
|
||||||
wallet.set_verifier(verifier)
|
|
||||||
synchronizer = WalletSynchronizer(wallet, config)
|
|
||||||
synchronizer.start()
|
|
||||||
|
|
||||||
if not found and a == 'restore' and s is not None:
|
|
||||||
try:
|
|
||||||
keep_it = gui.restore_wallet()
|
|
||||||
wallet.fill_addressbook()
|
|
||||||
except:
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc(file=sys.stdout)
|
|
||||||
exit()
|
|
||||||
|
|
||||||
if not keep_it: exit()
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
gui.password_dialog()
|
|
||||||
|
|
||||||
#wallet.save()
|
|
||||||
gui.main(url)
|
gui.main(url)
|
||||||
#wallet.save()
|
|
||||||
|
|
||||||
verifier.stop()
|
|
||||||
synchronizer.stop()
|
|
||||||
interface.stop()
|
|
||||||
|
|
||||||
# we use daemon threads, their termination is enforced.
|
# we use daemon threads, their termination is enforced.
|
||||||
# this sleep command gives them time to terminate cleanly.
|
# this sleep command gives them time to terminate cleanly.
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
# instanciate wallet for command-line
|
||||||
|
wallet = Wallet(config)
|
||||||
|
|
||||||
if cmd not in known_commands:
|
if cmd not in known_commands:
|
||||||
cmd = 'help'
|
cmd = 'help'
|
||||||
|
|
||||||
|
@ -338,6 +274,16 @@ if __name__ == '__main__':
|
||||||
domain = [options.from_addr] if options.from_addr else None
|
domain = [options.from_addr] if options.from_addr else None
|
||||||
args = [ 'mktx', args[1], Decimal(args[2]), Decimal(options.tx_fee) if options.tx_fee else None, options.change_addr, domain ]
|
args = [ 'mktx', args[1], Decimal(args[2]), Decimal(options.tx_fee) if options.tx_fee else None, options.change_addr, domain ]
|
||||||
|
|
||||||
|
elif cmd in ['paytomany', 'mksendmanytx']:
|
||||||
|
domain = [options.from_addr] if options.from_addr else None
|
||||||
|
outputs = []
|
||||||
|
for i in range(1, len(args), 2):
|
||||||
|
if len(args) < i+2:
|
||||||
|
print_msg("Error: Mismatched arguments.")
|
||||||
|
exit(1)
|
||||||
|
outputs.append((args[i], Decimal(args[i+1])))
|
||||||
|
args = [ 'mksendmanytx', outputs, Decimal(options.tx_fee) if options.tx_fee else None, options.change_addr, domain ]
|
||||||
|
|
||||||
elif cmd == 'help':
|
elif cmd == 'help':
|
||||||
if len(args) < 2:
|
if len(args) < 2:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
|
@ -429,7 +375,8 @@ if __name__ == '__main__':
|
||||||
try:
|
try:
|
||||||
result = func(*args[1:])
|
result = func(*args[1:])
|
||||||
except BaseException, e:
|
except BaseException, e:
|
||||||
print_msg("Error: " + str(e))
|
import traceback
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if type(result) == str:
|
if type(result) == str:
|
||||||
|
|
|
@ -42,7 +42,7 @@ except:
|
||||||
from electrum.wallet import format_satoshis
|
from electrum.wallet import format_satoshis
|
||||||
from electrum.bitcoin import Transaction, is_valid
|
from electrum.bitcoin import Transaction, is_valid
|
||||||
from electrum import mnemonic
|
from electrum import mnemonic
|
||||||
from electrum import util, bitcoin, commands
|
from electrum import util, bitcoin, commands, Interface, Wallet, WalletVerifier, WalletSynchronizer
|
||||||
|
|
||||||
import bmp, pyqrnative
|
import bmp, pyqrnative
|
||||||
import exchange_rate
|
import exchange_rate
|
||||||
|
@ -265,6 +265,7 @@ class ElectrumWindow(QMainWindow):
|
||||||
if reason == QSystemTrayIcon.DoubleClick:
|
if reason == QSystemTrayIcon.DoubleClick:
|
||||||
self.showNormal()
|
self.showNormal()
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, wallet, config):
|
def __init__(self, wallet, config):
|
||||||
QMainWindow.__init__(self)
|
QMainWindow.__init__(self)
|
||||||
self._close_electrum = False
|
self._close_electrum = False
|
||||||
|
@ -352,10 +353,21 @@ class ElectrumWindow(QMainWindow):
|
||||||
wallet_folder = self.wallet.config.path
|
wallet_folder = self.wallet.config.path
|
||||||
re.sub("(\/\w*.dat)$", "", wallet_folder)
|
re.sub("(\/\w*.dat)$", "", wallet_folder)
|
||||||
file_name = QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder, "*.dat")
|
file_name = QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder, "*.dat")
|
||||||
if not file_name:
|
return file_name
|
||||||
return
|
|
||||||
else:
|
def open_wallet(self):
|
||||||
self.load_wallet(file_name)
|
n = self.select_wallet_file()
|
||||||
|
if n:
|
||||||
|
self.load_wallet(n)
|
||||||
|
|
||||||
|
def new_wallet(self):
|
||||||
|
n = self.getOpenFileName("Select wallet file")
|
||||||
|
|
||||||
|
wizard = installwizard.InstallWizard(self.config, self.interface)
|
||||||
|
wallet = wizard.run()
|
||||||
|
if wallet:
|
||||||
|
self.load_wallet(wallet)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def init_menubar(self):
|
def init_menubar(self):
|
||||||
|
@ -363,7 +375,10 @@ class ElectrumWindow(QMainWindow):
|
||||||
|
|
||||||
electrum_menu = menubar.addMenu(_("&File"))
|
electrum_menu = menubar.addMenu(_("&File"))
|
||||||
open_wallet_action = electrum_menu.addAction(_("Open wallet"))
|
open_wallet_action = electrum_menu.addAction(_("Open wallet"))
|
||||||
open_wallet_action.triggered.connect(self.select_wallet_file)
|
open_wallet_action.triggered.connect(self.open_wallet)
|
||||||
|
|
||||||
|
new_wallet_action = electrum_menu.addAction(_("New wallet"))
|
||||||
|
new_wallet_action.triggered.connect(self.new_wallet)
|
||||||
|
|
||||||
preferences_name = _("Preferences")
|
preferences_name = _("Preferences")
|
||||||
if sys.platform == 'darwin':
|
if sys.platform == 'darwin':
|
||||||
|
@ -430,6 +445,7 @@ class ElectrumWindow(QMainWindow):
|
||||||
|
|
||||||
self.setMenuBar(menubar)
|
self.setMenuBar(menubar)
|
||||||
|
|
||||||
|
|
||||||
def load_wallet(self, filename):
|
def load_wallet(self, filename):
|
||||||
import electrum
|
import electrum
|
||||||
|
|
||||||
|
@ -1268,7 +1284,7 @@ class ElectrumWindow(QMainWindow):
|
||||||
account_items = []
|
account_items = []
|
||||||
|
|
||||||
for k, account in account_items:
|
for k, account in account_items:
|
||||||
name = account.get_name()
|
name = self.wallet.labels.get(k, 'unnamed account')
|
||||||
c,u = self.wallet.get_account_balance(k)
|
c,u = self.wallet.get_account_balance(k)
|
||||||
account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
|
account_item = QTreeWidgetItem( [ name, '', self.format_amount(c+u), ''] )
|
||||||
l.addTopLevelItem(account_item)
|
l.addTopLevelItem(account_item)
|
||||||
|
@ -1395,7 +1411,7 @@ class ElectrumWindow(QMainWindow):
|
||||||
sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
|
sb.addPermanentWidget( StatusBarButton( QIcon(":icons/switchgui.png"), _("Switch to Lite Mode"), self.go_lite ) )
|
||||||
if self.wallet.seed:
|
if self.wallet.seed:
|
||||||
self.lock_icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
|
self.lock_icon = QIcon(":icons/lock.png") if self.wallet.use_encryption else QIcon(":icons/unlock.png")
|
||||||
self.password_button = StatusBarButton( self.lock_icon, _("Password"), lambda: self.change_password_dialog(self.wallet, self) )
|
self.password_button = StatusBarButton( self.lock_icon, _("Password"), self.change_password_dialog )
|
||||||
sb.addPermanentWidget( self.password_button )
|
sb.addPermanentWidget( self.password_button )
|
||||||
sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
|
sb.addPermanentWidget( StatusBarButton( QIcon(":icons/preferences.png"), _("Preferences"), self.settings_dialog ) )
|
||||||
if self.wallet.seed:
|
if self.wallet.seed:
|
||||||
|
@ -1407,6 +1423,13 @@ class ElectrumWindow(QMainWindow):
|
||||||
|
|
||||||
self.setStatusBar(sb)
|
self.setStatusBar(sb)
|
||||||
|
|
||||||
|
|
||||||
|
def change_password_dialog(self):
|
||||||
|
from password_dialog import PasswordDialog
|
||||||
|
d = PasswordDialog(self.wallet, self)
|
||||||
|
d.run()
|
||||||
|
|
||||||
|
|
||||||
def go_lite(self):
|
def go_lite(self):
|
||||||
import gui_lite
|
import gui_lite
|
||||||
self.config.set_key('gui', 'lite', True)
|
self.config.set_key('gui', 'lite', True)
|
||||||
|
@ -1490,63 +1513,12 @@ class ElectrumWindow(QMainWindow):
|
||||||
except:
|
except:
|
||||||
QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
|
QMessageBox.warning(self, _('Error'), _('Incorrect Password'), _('OK'))
|
||||||
return
|
return
|
||||||
self.show_seed(seed, self.wallet.imported_keys, self)
|
|
||||||
|
from seed_dialog import SeedDialog
|
||||||
|
d = SeedDialog(self)
|
||||||
|
d.show_seed(seed, self.wallet.imported_keys)
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def show_seed(self, seed, imported_keys, parent=None):
|
|
||||||
dialog = QDialog(parent)
|
|
||||||
dialog.setModal(1)
|
|
||||||
dialog.setWindowTitle('Electrum' + ' - ' + _('Seed'))
|
|
||||||
|
|
||||||
brainwallet = ' '.join(mnemonic.mn_encode(seed))
|
|
||||||
|
|
||||||
label1 = QLabel(_("Your wallet generation seed is")+ ":")
|
|
||||||
|
|
||||||
seed_text = QTextEdit(brainwallet)
|
|
||||||
seed_text.setReadOnly(True)
|
|
||||||
seed_text.setMaximumHeight(130)
|
|
||||||
|
|
||||||
msg2 = _("Please write down or memorize these 12 words (order is important).") + " " \
|
|
||||||
+ _("This seed will allow you to recover your wallet in case of computer failure.") + " " \
|
|
||||||
+ _("Your seed is also displayed as QR code, in case you want to transfer it to a mobile phone.") + "<p>" \
|
|
||||||
+ "<b>"+_("WARNING")+":</b> " + _("Never disclose your seed. Never type it on a website.") + "</b><p>"
|
|
||||||
if imported_keys:
|
|
||||||
msg2 += "<b>"+_("WARNING")+":</b> " + _("Your wallet contains imported keys. These keys cannot be recovered from seed.") + "</b><p>"
|
|
||||||
label2 = QLabel(msg2)
|
|
||||||
label2.setWordWrap(True)
|
|
||||||
|
|
||||||
logo = QLabel()
|
|
||||||
logo.setPixmap(QPixmap(":icons/seed.png").scaledToWidth(56))
|
|
||||||
logo.setMaximumWidth(60)
|
|
||||||
|
|
||||||
qrw = QRCodeWidget(seed)
|
|
||||||
|
|
||||||
ok_button = QPushButton(_("OK"))
|
|
||||||
ok_button.setDefault(True)
|
|
||||||
ok_button.clicked.connect(dialog.accept)
|
|
||||||
|
|
||||||
grid = QGridLayout()
|
|
||||||
#main_layout.addWidget(logo, 0, 0)
|
|
||||||
|
|
||||||
grid.addWidget(logo, 0, 0)
|
|
||||||
grid.addWidget(label1, 0, 1)
|
|
||||||
|
|
||||||
grid.addWidget(seed_text, 1, 0, 1, 2)
|
|
||||||
|
|
||||||
grid.addWidget(qrw, 0, 2, 2, 1)
|
|
||||||
|
|
||||||
vbox = QVBoxLayout()
|
|
||||||
vbox.addLayout(grid)
|
|
||||||
vbox.addWidget(label2)
|
|
||||||
|
|
||||||
hbox = QHBoxLayout()
|
|
||||||
hbox.addStretch(1)
|
|
||||||
hbox.addWidget(ok_button)
|
|
||||||
vbox.addLayout(hbox)
|
|
||||||
|
|
||||||
dialog.setLayout(vbox)
|
|
||||||
dialog.exec_()
|
|
||||||
|
|
||||||
def show_qrcode(self, data, title = "QR code"):
|
def show_qrcode(self, data, title = "QR code"):
|
||||||
if not data: return
|
if not data: return
|
||||||
|
@ -1728,79 +1700,6 @@ class ElectrumWindow(QMainWindow):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def change_password_dialog( wallet, parent=None ):
|
|
||||||
|
|
||||||
if not wallet.seed:
|
|
||||||
QMessageBox.information(parent, _('Error'), _('No seed'), _('OK'))
|
|
||||||
return
|
|
||||||
|
|
||||||
d = QDialog(parent)
|
|
||||||
d.setModal(1)
|
|
||||||
|
|
||||||
pw = QLineEdit()
|
|
||||||
pw.setEchoMode(2)
|
|
||||||
new_pw = QLineEdit()
|
|
||||||
new_pw.setEchoMode(2)
|
|
||||||
conf_pw = QLineEdit()
|
|
||||||
conf_pw.setEchoMode(2)
|
|
||||||
|
|
||||||
vbox = QVBoxLayout()
|
|
||||||
if parent:
|
|
||||||
msg = (_('Your wallet is encrypted. Use this dialog to change your password.')+'\n'\
|
|
||||||
+_('To disable wallet encryption, enter an empty new password.')) \
|
|
||||||
if wallet.use_encryption else _('Your wallet keys are not encrypted')
|
|
||||||
else:
|
|
||||||
msg = _("Please choose a password to encrypt your wallet keys.")+'\n'\
|
|
||||||
+_("Leave these fields empty if you want to disable encryption.")
|
|
||||||
vbox.addWidget(QLabel(msg))
|
|
||||||
|
|
||||||
grid = QGridLayout()
|
|
||||||
grid.setSpacing(8)
|
|
||||||
|
|
||||||
if wallet.use_encryption:
|
|
||||||
grid.addWidget(QLabel(_('Password')), 1, 0)
|
|
||||||
grid.addWidget(pw, 1, 1)
|
|
||||||
|
|
||||||
grid.addWidget(QLabel(_('New Password')), 2, 0)
|
|
||||||
grid.addWidget(new_pw, 2, 1)
|
|
||||||
|
|
||||||
grid.addWidget(QLabel(_('Confirm Password')), 3, 0)
|
|
||||||
grid.addWidget(conf_pw, 3, 1)
|
|
||||||
vbox.addLayout(grid)
|
|
||||||
|
|
||||||
vbox.addLayout(ok_cancel_buttons(d))
|
|
||||||
d.setLayout(vbox)
|
|
||||||
|
|
||||||
if not d.exec_(): return
|
|
||||||
|
|
||||||
password = unicode(pw.text()) if wallet.use_encryption else None
|
|
||||||
new_password = unicode(new_pw.text())
|
|
||||||
new_password2 = unicode(conf_pw.text())
|
|
||||||
|
|
||||||
try:
|
|
||||||
seed = wallet.decode_seed(password)
|
|
||||||
except:
|
|
||||||
QMessageBox.warning(parent, _('Error'), _('Incorrect Password'), _('OK'))
|
|
||||||
return
|
|
||||||
|
|
||||||
if new_password != new_password2:
|
|
||||||
QMessageBox.warning(parent, _('Error'), _('Passwords do not match'), _('OK'))
|
|
||||||
return ElectrumWindow.change_password_dialog(wallet, parent) # Retry
|
|
||||||
|
|
||||||
try:
|
|
||||||
wallet.update_password(seed, password, new_password)
|
|
||||||
except:
|
|
||||||
QMessageBox.warning(parent, _('Error'), _('Failed to update password'), _('OK'))
|
|
||||||
return
|
|
||||||
|
|
||||||
QMessageBox.information(parent, _('Success'), _('Password was updated successfully'), _('OK'))
|
|
||||||
|
|
||||||
if parent:
|
|
||||||
icon = QIcon(":icons/lock.png") if wallet.use_encryption else QIcon(":icons/unlock.png")
|
|
||||||
parent.password_button.setIcon( icon )
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def generate_transaction_information_widget(self, tx):
|
def generate_transaction_information_widget(self, tx):
|
||||||
tabs = QTabWidget(self)
|
tabs = QTabWidget(self)
|
||||||
|
@ -2282,10 +2181,13 @@ class OpenFileEventFilter(QObject):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ElectrumGui:
|
class ElectrumGui:
|
||||||
|
|
||||||
def __init__(self, wallet, config, app=None):
|
def __init__(self, config, app=None):
|
||||||
self.wallet = wallet
|
self.interface = Interface(config, True)
|
||||||
self.config = config
|
self.config = config
|
||||||
self.windows = []
|
self.windows = []
|
||||||
self.efilter = OpenFileEventFilter(self.windows)
|
self.efilter = OpenFileEventFilter(self.windows)
|
||||||
|
@ -2293,116 +2195,32 @@ class ElectrumGui:
|
||||||
self.app = QApplication(sys.argv)
|
self.app = QApplication(sys.argv)
|
||||||
self.app.installEventFilter(self.efilter)
|
self.app.installEventFilter(self.efilter)
|
||||||
|
|
||||||
def restore_or_create(self):
|
|
||||||
msg = _("Wallet file not found.")+"\n"+_("Do you want to create a new wallet, or to restore an existing one?")
|
|
||||||
r = QMessageBox.question(None, _('Message'), msg, _('Create'), _('Restore'), _('Cancel'), 0, 2)
|
|
||||||
if r==2: return None
|
|
||||||
return 'restore' if r==1 else 'create'
|
|
||||||
|
|
||||||
|
def main(self, url):
|
||||||
|
|
||||||
def verify_seed(self):
|
found = self.config.wallet_file_exists
|
||||||
r = self.seed_dialog(False)
|
if not found:
|
||||||
if r != self.wallet.seed:
|
import installwizard
|
||||||
QMessageBox.warning(None, _('Error'), 'incorrect seed', 'OK')
|
wizard = installwizard.InstallWizard(self.config, self.interface)
|
||||||
return False
|
wallet = wizard.run()
|
||||||
|
if not wallet:
|
||||||
|
exit()
|
||||||
else:
|
else:
|
||||||
return True
|
wallet = Wallet(self.config)
|
||||||
|
|
||||||
|
self.wallet = wallet
|
||||||
|
|
||||||
|
self.interface.start(wait = False)
|
||||||
|
self.interface.send([('server.peers.subscribe',[])])
|
||||||
|
wallet.interface = self.interface
|
||||||
|
|
||||||
|
verifier = WalletVerifier(self.interface, self.config)
|
||||||
|
verifier.start()
|
||||||
|
wallet.set_verifier(verifier)
|
||||||
|
synchronizer = WalletSynchronizer(wallet, self.config)
|
||||||
|
synchronizer.start()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def seed_dialog(self, is_restore=True):
|
|
||||||
d = QDialog()
|
|
||||||
d.setModal(1)
|
|
||||||
|
|
||||||
vbox = QVBoxLayout()
|
|
||||||
if is_restore:
|
|
||||||
msg = _("Please enter your wallet seed (or your master public key if you want to create a watching-only wallet)." + ' ')
|
|
||||||
else:
|
|
||||||
msg = _("Your seed is important! To make sure that you have properly saved your seed, please type it here." + ' ')
|
|
||||||
|
|
||||||
msg += _("Your seed can be entered as a sequence of words, or as a hexadecimal string."+ '\n')
|
|
||||||
|
|
||||||
label=QLabel(msg)
|
|
||||||
label.setWordWrap(True)
|
|
||||||
vbox.addWidget(label)
|
|
||||||
|
|
||||||
seed_e = QTextEdit()
|
|
||||||
seed_e.setMaximumHeight(100)
|
|
||||||
vbox.addWidget(seed_e)
|
|
||||||
|
|
||||||
if is_restore:
|
|
||||||
grid = QGridLayout()
|
|
||||||
grid.setSpacing(8)
|
|
||||||
gap_e = AmountEdit(None, True)
|
|
||||||
gap_e.setText("5")
|
|
||||||
grid.addWidget(QLabel(_('Gap limit')), 2, 0)
|
|
||||||
grid.addWidget(gap_e, 2, 1)
|
|
||||||
grid.addWidget(HelpButton(_('Keep the default value unless you modified this parameter in your wallet.')), 2, 3)
|
|
||||||
vbox.addLayout(grid)
|
|
||||||
|
|
||||||
vbox.addLayout(ok_cancel_buttons(d))
|
|
||||||
d.setLayout(vbox)
|
|
||||||
|
|
||||||
if not d.exec_(): return
|
|
||||||
|
|
||||||
try:
|
|
||||||
seed = str(seed_e.toPlainText())
|
|
||||||
seed.decode('hex')
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
seed = mnemonic.mn_decode( seed.split() )
|
|
||||||
except:
|
|
||||||
QMessageBox.warning(None, _('Error'), _('I cannot decode this'), _('OK'))
|
|
||||||
return
|
|
||||||
|
|
||||||
if not seed:
|
|
||||||
QMessageBox.warning(None, _('Error'), _('No seed'), _('OK'))
|
|
||||||
return
|
|
||||||
|
|
||||||
if not is_restore:
|
|
||||||
return seed
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
gap = int(unicode(gap_e.text()))
|
|
||||||
except:
|
|
||||||
QMessageBox.warning(None, _('Error'), 'error', 'OK')
|
|
||||||
return
|
|
||||||
return seed, gap
|
|
||||||
|
|
||||||
|
|
||||||
def network_dialog(self):
|
|
||||||
return NetworkDialog(self.wallet.interface, self.config, None).do_exec()
|
|
||||||
|
|
||||||
|
|
||||||
def show_seed(self):
|
|
||||||
ElectrumWindow.show_seed(self.wallet.seed, self.wallet.imported_keys)
|
|
||||||
|
|
||||||
def password_dialog(self):
|
|
||||||
if self.wallet.seed:
|
|
||||||
ElectrumWindow.change_password_dialog(self.wallet)
|
|
||||||
|
|
||||||
|
|
||||||
def restore_wallet(self):
|
|
||||||
wallet = self.wallet
|
|
||||||
# wait until we are connected, because the user might have selected another server
|
|
||||||
if not wallet.interface.is_connected:
|
|
||||||
waiting = lambda: False if wallet.interface.is_connected else "%s \n" % (_("Connecting..."))
|
|
||||||
waiting_dialog(waiting)
|
|
||||||
|
|
||||||
waiting = lambda: False if wallet.is_up_to_date() else "%s\n%s %d\n%s %.1f"\
|
|
||||||
%(_("Please wait..."),_("Addresses generated:"),len(wallet.addresses(True)),_("Kilobytes received:"), wallet.interface.bytes_received/1024.)
|
|
||||||
|
|
||||||
wallet.set_up_to_date(False)
|
|
||||||
wallet.interface.poke('synchronizer')
|
|
||||||
waiting_dialog(waiting)
|
|
||||||
if wallet.is_found():
|
|
||||||
print_error( "Recovery successful" )
|
|
||||||
else:
|
|
||||||
QMessageBox.information(None, _('Error'), _("No transactions found for this seed"), _('OK'))
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def main(self,url):
|
|
||||||
s = Timer()
|
s = Timer()
|
||||||
s.start()
|
s.start()
|
||||||
w = ElectrumWindow(self.wallet, self.config)
|
w = ElectrumWindow(self.wallet, self.config)
|
||||||
|
@ -2415,4 +2233,8 @@ class ElectrumGui:
|
||||||
|
|
||||||
self.app.exec_()
|
self.app.exec_()
|
||||||
|
|
||||||
|
verifier.stop()
|
||||||
|
synchronizer.stop()
|
||||||
|
self.interface.stop()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
@ -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 )
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ class BasePlugin:
|
||||||
|
|
||||||
def __init__(self, gui, name):
|
def __init__(self, gui, name):
|
||||||
self.gui = gui
|
self.gui = gui
|
||||||
|
self.wallet = self.gui.wallet
|
||||||
self.name = name
|
self.name = name
|
||||||
self.config = gui.config
|
self.config = gui.config
|
||||||
|
|
||||||
|
|
|
@ -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_()
|
|
@ -24,13 +24,9 @@ class Account(object):
|
||||||
def __init__(self, v):
|
def __init__(self, v):
|
||||||
self.addresses = v.get('0', [])
|
self.addresses = v.get('0', [])
|
||||||
self.change = v.get('1', [])
|
self.change = v.get('1', [])
|
||||||
self.name = v.get('name', 'unnamed')
|
|
||||||
|
|
||||||
def dump(self):
|
def dump(self):
|
||||||
return {'0':self.addresses, '1':self.change, 'name':self.name}
|
return {'0':self.addresses, '1':self.change}
|
||||||
|
|
||||||
def get_name(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def get_addresses(self, for_change):
|
def get_addresses(self, for_change):
|
||||||
return self.change[:] if for_change else self.addresses[:]
|
return self.change[:] if for_change else self.addresses[:]
|
||||||
|
@ -171,25 +167,9 @@ class BIP32_Account(Account):
|
||||||
K, K_compressed, chain = CKD_prime(K, chain, i)
|
K, K_compressed, chain = CKD_prime(K, chain, i)
|
||||||
return K_compressed.encode('hex')
|
return K_compressed.encode('hex')
|
||||||
|
|
||||||
def get_private_key(self, sequence, master_k):
|
def redeem_script(self, sequence):
|
||||||
chain = self.c
|
return None
|
||||||
k = master_k
|
|
||||||
for i in sequence:
|
|
||||||
k, chain = CKD(k, chain, i)
|
|
||||||
return SecretToASecret(k, True)
|
|
||||||
|
|
||||||
def get_private_keys(self, sequence_list, seed):
|
|
||||||
return [ self.get_private_key( sequence, seed) for sequence in sequence_list]
|
|
||||||
|
|
||||||
def check_seed(self, seed):
|
|
||||||
master_secret, master_chain, master_public_key, master_public_key_compressed = bip32_init(seed)
|
|
||||||
assert self.mpk == (master_public_key.encode('hex'), master_chain.encode('hex'))
|
|
||||||
|
|
||||||
def get_input_info(self, sequence):
|
|
||||||
chain, i = sequence
|
|
||||||
pk_addr = self.get_address(chain, i)
|
|
||||||
redeemScript = None
|
|
||||||
return pk_addr, redeemScript
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -215,18 +195,44 @@ class BIP32_Account_2of2(BIP32_Account):
|
||||||
K, K_compressed, chain = CKD_prime(K, chain, i)
|
K, K_compressed, chain = CKD_prime(K, chain, i)
|
||||||
return K_compressed.encode('hex')
|
return K_compressed.encode('hex')
|
||||||
|
|
||||||
def get_address(self, for_change, n):
|
def redeem_script(self, sequence):
|
||||||
pubkey1 = self.get_pubkey(for_change, n)
|
|
||||||
pubkey2 = self.get_pubkey2(for_change, n)
|
|
||||||
address = Transaction.multisig_script([pubkey1, pubkey2], 2)["address"]
|
|
||||||
return address
|
|
||||||
|
|
||||||
def get_input_info(self, sequence):
|
|
||||||
chain, i = sequence
|
chain, i = sequence
|
||||||
pubkey1 = self.get_pubkey(chain, i)
|
pubkey1 = self.get_pubkey(chain, i)
|
||||||
pubkey2 = self.get_pubkey2(chain, i)
|
pubkey2 = self.get_pubkey2(chain, i)
|
||||||
# fixme
|
return Transaction.multisig_script([pubkey1, pubkey2], 2)
|
||||||
pk_addr = None # public_key_to_bc_address( pubkey1 ) # we need to return that address to get the right private key
|
|
||||||
redeemScript = Transaction.multisig_script([pubkey1, pubkey2], 2)['redeemScript']
|
def get_address(self, for_change, n):
|
||||||
return pk_addr, redeemScript
|
address = hash_160_to_bc_address(hash_160(self.redeem_script((for_change, n)).decode('hex')), 5)
|
||||||
|
return address
|
||||||
|
|
||||||
|
|
||||||
|
class BIP32_Account_2of3(BIP32_Account_2of2):
|
||||||
|
|
||||||
|
def __init__(self, v):
|
||||||
|
BIP32_Account_2of2.__init__(self, v)
|
||||||
|
self.c3 = v['c3'].decode('hex')
|
||||||
|
self.K3 = v['K3'].decode('hex')
|
||||||
|
self.cK3 = v['cK3'].decode('hex')
|
||||||
|
|
||||||
|
def dump(self):
|
||||||
|
d = BIP32_Account_2of2.dump(self)
|
||||||
|
d['c3'] = self.c3.encode('hex')
|
||||||
|
d['K3'] = self.K3.encode('hex')
|
||||||
|
d['cK3'] = self.cK3.encode('hex')
|
||||||
|
return d
|
||||||
|
|
||||||
|
def get_pubkey3(self, for_change, n):
|
||||||
|
K = self.K3
|
||||||
|
chain = self.c3
|
||||||
|
for i in [for_change, n]:
|
||||||
|
K, K_compressed, chain = CKD_prime(K, chain, i)
|
||||||
|
return K_compressed.encode('hex')
|
||||||
|
|
||||||
|
def get_redeem_script(self, sequence):
|
||||||
|
chain, i = sequence
|
||||||
|
pubkey1 = self.get_pubkey(chain, i)
|
||||||
|
pubkey2 = self.get_pubkey2(chain, i)
|
||||||
|
pubkey3 = self.get_pubkey3(chain, i)
|
||||||
|
return Transaction.multisig_script([pubkey1, pubkey2, pubkey3], 3)
|
||||||
|
|
||||||
|
|
||||||
|
|
138
lib/bitcoin.py
138
lib/bitcoin.py
|
@ -244,17 +244,17 @@ def is_compressed(sec):
|
||||||
return len(b) == 33
|
return len(b) == 33
|
||||||
|
|
||||||
|
|
||||||
def address_from_private_key(sec):
|
def public_key_from_private_key(sec):
|
||||||
# rebuild public key from private key, compressed or uncompressed
|
# rebuild public key from private key, compressed or uncompressed
|
||||||
pkey = regenerate_key(sec)
|
pkey = regenerate_key(sec)
|
||||||
assert pkey
|
assert pkey
|
||||||
|
|
||||||
# figure out if private key is compressed
|
|
||||||
compressed = is_compressed(sec)
|
compressed = is_compressed(sec)
|
||||||
|
|
||||||
# rebuild private and public key from regenerated secret
|
|
||||||
private_key = GetPrivKey(pkey, compressed)
|
|
||||||
public_key = GetPubKey(pkey.pubkey, compressed)
|
public_key = GetPubKey(pkey.pubkey, compressed)
|
||||||
|
return public_key.encode('hex')
|
||||||
|
|
||||||
|
|
||||||
|
def address_from_private_key(sec):
|
||||||
|
public_key = public_key_from_private_key(sec)
|
||||||
address = public_key_to_bc_address(public_key)
|
address = public_key_to_bc_address(public_key)
|
||||||
return address
|
return address
|
||||||
|
|
||||||
|
@ -448,6 +448,11 @@ def bip32_public_derivation(c, K, branch, sequence):
|
||||||
return c.encode('hex'), K.encode('hex'), cK.encode('hex')
|
return c.encode('hex'), K.encode('hex'), cK.encode('hex')
|
||||||
|
|
||||||
|
|
||||||
|
def bip32_private_key(sequence, k, chain):
|
||||||
|
for i in sequence:
|
||||||
|
k, chain = CKD(k, chain, i)
|
||||||
|
return SecretToASecret(k, True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -508,8 +513,7 @@ class Transaction:
|
||||||
raise
|
raise
|
||||||
s += 'ae'
|
s += 'ae'
|
||||||
|
|
||||||
out = { "address": hash_160_to_bc_address(hash_160(s.decode('hex')), 5), "redeemScript":s }
|
return s
|
||||||
return out
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def serialize( klass, inputs, outputs, for_sig = None ):
|
def serialize( klass, inputs, outputs, for_sig = None ):
|
||||||
|
@ -522,24 +526,24 @@ class Transaction:
|
||||||
s += int_to_hex(txin['index'],4) # prev index
|
s += int_to_hex(txin['index'],4) # prev index
|
||||||
|
|
||||||
if for_sig is None:
|
if for_sig is None:
|
||||||
pubkeysig = txin.get('pubkeysig')
|
signatures = txin['signatures']
|
||||||
if pubkeysig:
|
pubkeys = txin['pubkeys']
|
||||||
pubkey, sig = pubkeysig[0]
|
if not txin.get('redeemScript'):
|
||||||
sig = sig + chr(1) # hashtype
|
pubkey = pubkeys[0]
|
||||||
script = op_push( len(sig))
|
sig = signatures[0]
|
||||||
script += sig.encode('hex')
|
sig = sig + '01' # hashtype
|
||||||
script += op_push( len(pubkey))
|
script = op_push(len(sig)/2)
|
||||||
script += pubkey.encode('hex')
|
script += sig
|
||||||
|
script += op_push(len(pubkey)/2)
|
||||||
|
script += pubkey
|
||||||
else:
|
else:
|
||||||
signatures = txin['signatures']
|
|
||||||
pubkeys = txin['pubkeys']
|
|
||||||
script = '00' # op_0
|
script = '00' # op_0
|
||||||
for sig in signatures:
|
for sig in signatures:
|
||||||
sig = sig + '01'
|
sig = sig + '01'
|
||||||
script += op_push(len(sig)/2)
|
script += op_push(len(sig)/2)
|
||||||
script += sig
|
script += sig
|
||||||
|
|
||||||
redeem_script = klass.multisig_script(pubkeys,2).get('redeemScript')
|
redeem_script = klass.multisig_script(pubkeys,2)
|
||||||
script += op_push(len(redeem_script)/2)
|
script += op_push(len(redeem_script)/2)
|
||||||
script += redeem_script
|
script += redeem_script
|
||||||
|
|
||||||
|
@ -587,79 +591,47 @@ class Transaction:
|
||||||
def hash(self):
|
def hash(self):
|
||||||
return Hash(self.raw.decode('hex') )[::-1].encode('hex')
|
return Hash(self.raw.decode('hex') )[::-1].encode('hex')
|
||||||
|
|
||||||
def sign(self, private_keys):
|
|
||||||
|
|
||||||
|
def sign(self, keypairs):
|
||||||
import deserialize
|
import deserialize
|
||||||
|
is_complete = True
|
||||||
|
print_error("tx.sign(), keypairs:", keypairs)
|
||||||
|
|
||||||
for i in range(len(self.inputs)):
|
for i, txin in enumerate(self.inputs):
|
||||||
txin = self.inputs[i]
|
|
||||||
tx_for_sig = self.serialize( self.inputs, self.outputs, for_sig = i )
|
|
||||||
|
|
||||||
|
# if the input is multisig, parse redeem script
|
||||||
redeem_script = txin.get('redeemScript')
|
redeem_script = txin.get('redeemScript')
|
||||||
if redeem_script:
|
num, redeem_pubkeys = deserialize.parse_redeemScript(redeem_script) if redeem_script else (1, [txin.get('redeemPubkey')])
|
||||||
# 1 parse the redeem script
|
|
||||||
num, redeem_pubkeys = deserialize.parse_redeemScript(redeem_script)
|
|
||||||
self.inputs[i]["pubkeys"] = redeem_pubkeys
|
|
||||||
|
|
||||||
# build list of public/private keys
|
# add pubkeys
|
||||||
keypairs = {}
|
txin["pubkeys"] = redeem_pubkeys
|
||||||
for sec in private_keys.values():
|
# get list of already existing signatures
|
||||||
|
signatures = txin.get("signatures",[])
|
||||||
|
# continue if this txin is complete
|
||||||
|
if len(signatures) == num:
|
||||||
|
continue
|
||||||
|
|
||||||
|
tx_for_sig = self.serialize( self.inputs, self.outputs, for_sig = i )
|
||||||
|
for pubkey in redeem_pubkeys:
|
||||||
|
# check if we have the corresponding private key
|
||||||
|
if pubkey in keypairs.keys():
|
||||||
|
# add signature
|
||||||
|
sec = keypairs[pubkey]
|
||||||
compressed = is_compressed(sec)
|
compressed = is_compressed(sec)
|
||||||
pkey = regenerate_key(sec)
|
pkey = regenerate_key(sec)
|
||||||
pubkey = GetPubKey(pkey.pubkey, compressed)
|
secexp = pkey.secret
|
||||||
keypairs[ pubkey.encode('hex') ] = sec
|
private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
|
||||||
|
public_key = private_key.get_verifying_key()
|
||||||
|
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)
|
||||||
|
|
||||||
print "keypairs", keypairs
|
txin["signatures"] = signatures
|
||||||
print redeem_script, redeem_pubkeys
|
is_complete = is_complete and len(signatures) == num
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
|
self.is_complete = is_complete
|
||||||
self.raw = self.serialize( self.inputs, self.outputs )
|
self.raw = self.serialize( self.inputs, self.outputs )
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,9 @@ register_command('importprivkey', 1, 1, True, True, 'Import a private k
|
||||||
register_command('listaddresses', 3, 3, False, True, 'Returns your list of addresses.', '', listaddr_options)
|
register_command('listaddresses', 3, 3, False, True, 'Returns your list of addresses.', '', listaddr_options)
|
||||||
register_command('listunspent', 0, 0, False, True, 'Returns a list of unspent inputs in your wallet.')
|
register_command('listunspent', 0, 0, False, True, 'Returns a list of unspent inputs in your wallet.')
|
||||||
register_command('mktx', 5, 5, True, True, 'Create a signed transaction', 'mktx <recipient> <amount> [label]', payto_options)
|
register_command('mktx', 5, 5, True, True, 'Create a signed transaction', 'mktx <recipient> <amount> [label]', payto_options)
|
||||||
|
register_command('mksendmanytx', 4, 4, True, True, 'Create a signed transaction', 'mksendmanytx <recipient> <amount> [<recipient> <amount> ...]', payto_options)
|
||||||
register_command('payto', 5, 5, True, False, 'Create and broadcast a transaction.', "payto <recipient> <amount> [label]\n<recipient> can be a bitcoin address or a label", payto_options)
|
register_command('payto', 5, 5, True, False, 'Create and broadcast a transaction.', "payto <recipient> <amount> [label]\n<recipient> can be a bitcoin address or a label", payto_options)
|
||||||
|
register_command('paytomany', 4, 4, True, False, 'Create and broadcast a transaction.', "paytomany <recipient> <amount> [<recipient> <amount> ...]\n<recipient> can be a bitcoin address or a label", payto_options)
|
||||||
register_command('password', 0, 0, True, True, 'Change your password')
|
register_command('password', 0, 0, True, True, 'Change your password')
|
||||||
register_command('prioritize', 1, 1, False, True, 'Coins at prioritized addresses are spent first.', 'prioritize <address>')
|
register_command('prioritize', 1, 1, False, True, 'Coins at prioritized addresses are spent first.', 'prioritize <address>')
|
||||||
register_command('restore', 0, 0, False, False, 'Restore a wallet', '', restore_options)
|
register_command('restore', 0, 0, False, False, 'Restore a wallet', '', restore_options)
|
||||||
|
@ -131,7 +133,9 @@ class Commands:
|
||||||
|
|
||||||
def createmultisig(self, num, pubkeys):
|
def createmultisig(self, num, pubkeys):
|
||||||
assert isinstance(pubkeys, list)
|
assert isinstance(pubkeys, list)
|
||||||
return Transaction.multisig_script(pubkeys, num)
|
redeem_script = Transaction.multisig_script(pubkeys, num)
|
||||||
|
address = hash_160_to_bc_address(hash_160(redeem_script.decode('hex')), 5)
|
||||||
|
return {'address':address, 'redeemScript':redeem_script}
|
||||||
|
|
||||||
def freeze(self,addr):
|
def freeze(self,addr):
|
||||||
return self.wallet.freeze(addr)
|
return self.wallet.freeze(addr)
|
||||||
|
@ -205,10 +209,11 @@ class Commands:
|
||||||
return self.wallet.verify_message(address, signature, message)
|
return self.wallet.verify_message(address, signature, message)
|
||||||
|
|
||||||
|
|
||||||
def _mktx(self, to_address, amount, fee = None, change_addr = None, domain = None):
|
def _mktx(self, outputs, fee = None, change_addr = None, domain = None):
|
||||||
|
|
||||||
if not is_valid(to_address):
|
for to_address, amount in outputs:
|
||||||
raise BaseException("Invalid Bitcoin address", to_address)
|
if not is_valid(to_address):
|
||||||
|
raise BaseException("Invalid Bitcoin address", to_address)
|
||||||
|
|
||||||
if change_addr:
|
if change_addr:
|
||||||
if not is_valid(change_addr):
|
if not is_valid(change_addr):
|
||||||
|
@ -223,25 +228,40 @@ class Commands:
|
||||||
raise BaseException("address not in wallet", addr)
|
raise BaseException("address not in wallet", addr)
|
||||||
|
|
||||||
for k, v in self.wallet.labels.items():
|
for k, v in self.wallet.labels.items():
|
||||||
if v == to_address:
|
|
||||||
to_address = k
|
|
||||||
print_msg("alias", to_address)
|
|
||||||
break
|
|
||||||
if change_addr and v == change_addr:
|
if change_addr and v == change_addr:
|
||||||
change_addr = k
|
change_addr = k
|
||||||
|
|
||||||
amount = int(100000000*amount)
|
final_outputs = []
|
||||||
|
for to_address, amount in outputs:
|
||||||
|
for k, v in self.wallet.labels.items():
|
||||||
|
if v == to_address:
|
||||||
|
to_address = k
|
||||||
|
print_msg("alias", to_address)
|
||||||
|
break
|
||||||
|
|
||||||
|
amount = int(100000000*amount)
|
||||||
|
final_outputs.append((to_address, amount))
|
||||||
|
|
||||||
if fee: fee = int(100000000*fee)
|
if fee: fee = int(100000000*fee)
|
||||||
return self.wallet.mktx( [(to_address, amount)], self.password, fee , change_addr, domain)
|
return self.wallet.mktx(final_outputs, self.password, fee , change_addr, domain)
|
||||||
|
|
||||||
|
|
||||||
def mktx(self, to_address, amount, fee = None, change_addr = None, domain = None):
|
def mktx(self, to_address, amount, fee = None, change_addr = None, domain = None):
|
||||||
tx = self._mktx(to_address, amount, fee, change_addr, domain)
|
tx = self._mktx([(to_address, amount)], fee, change_addr, domain)
|
||||||
|
return tx.as_dict()
|
||||||
|
|
||||||
|
def mksendmanytx(self, outputs, fee = None, change_addr = None, domain = None):
|
||||||
|
tx = self._mktx(outputs, fee, change_addr, domain)
|
||||||
return tx.as_dict()
|
return tx.as_dict()
|
||||||
|
|
||||||
|
|
||||||
def payto(self, to_address, amount, fee = None, change_addr = None, domain = None):
|
def payto(self, to_address, amount, fee = None, change_addr = None, domain = None):
|
||||||
tx = self._mktx(to_address, amount, fee, change_addr, domain)
|
tx = self._mktx([(to_address, amount)], fee, change_addr, domain)
|
||||||
|
r, h = self.wallet.sendtx( tx )
|
||||||
|
return h
|
||||||
|
|
||||||
|
def paytomany(self, outputs, fee = None, change_addr = None, domain = None):
|
||||||
|
tx = self._mktx(outputs, fee, change_addr, domain)
|
||||||
r, h = self.wallet.sendtx( tx )
|
r, h = self.wallet.sendtx( tx )
|
||||||
return h
|
return h
|
||||||
|
|
||||||
|
|
|
@ -346,7 +346,7 @@ def get_address_from_input_script(bytes):
|
||||||
|
|
||||||
redeemScript = decoded[-1][1]
|
redeemScript = decoded[-1][1]
|
||||||
num = len(match) - 2
|
num = len(match) - 2
|
||||||
signatures = map(lambda x:x[1].encode('hex'), decoded[1:-1])
|
signatures = map(lambda x:x[1][:-1].encode('hex'), decoded[1:-1])
|
||||||
|
|
||||||
dec2 = [ x for x in script_GetOp(redeemScript) ]
|
dec2 = [ x for x in script_GetOp(redeemScript) ]
|
||||||
|
|
||||||
|
|
283
lib/wallet.py
283
lib/wallet.py
|
@ -74,7 +74,7 @@ class Wallet:
|
||||||
self.seed_version = config.get('seed_version', SEED_VERSION)
|
self.seed_version = config.get('seed_version', SEED_VERSION)
|
||||||
self.gap_limit = config.get('gap_limit', 5)
|
self.gap_limit = config.get('gap_limit', 5)
|
||||||
self.use_change = config.get('use_change',True)
|
self.use_change = config.get('use_change',True)
|
||||||
self.fee = int(config.get('fee_per_kb',50000))
|
self.fee = int(config.get('fee_per_kb',20000))
|
||||||
self.num_zeros = int(config.get('num_zeros',0))
|
self.num_zeros = int(config.get('num_zeros',0))
|
||||||
self.use_encryption = config.get('use_encryption', False)
|
self.use_encryption = config.get('use_encryption', False)
|
||||||
self.seed = config.get('seed', '') # encrypted
|
self.seed = config.get('seed', '') # encrypted
|
||||||
|
@ -172,62 +172,112 @@ class Wallet:
|
||||||
|
|
||||||
master_k, master_c, master_K, master_cK = bip32_init(self.seed)
|
master_k, master_c, master_K, master_cK = bip32_init(self.seed)
|
||||||
|
|
||||||
|
# normal accounts
|
||||||
k0, c0, K0, cK0 = bip32_private_derivation(master_k, master_c, "m/", "m/0'/")
|
k0, c0, K0, cK0 = bip32_private_derivation(master_k, master_c, "m/", "m/0'/")
|
||||||
|
# p2sh 2of2
|
||||||
k1, c1, K1, cK1 = bip32_private_derivation(master_k, master_c, "m/", "m/1'/")
|
k1, c1, K1, cK1 = bip32_private_derivation(master_k, master_c, "m/", "m/1'/")
|
||||||
k2, c2, K2, cK2 = bip32_private_derivation(master_k, master_c, "m/", "m/2'/")
|
k2, c2, K2, cK2 = bip32_private_derivation(master_k, master_c, "m/", "m/2'/")
|
||||||
|
# p2sh 2of3
|
||||||
|
k3, c3, K3, cK3 = bip32_private_derivation(master_k, master_c, "m/", "m/3'/")
|
||||||
|
k4, c4, K4, cK4 = bip32_private_derivation(master_k, master_c, "m/", "m/4'/")
|
||||||
|
k5, c5, K5, cK5 = bip32_private_derivation(master_k, master_c, "m/", "m/5'/")
|
||||||
|
|
||||||
self.master_public_keys = {
|
self.master_public_keys = {
|
||||||
"m/0'/": (c0, K0, cK0),
|
"m/0'/": (c0, K0, cK0),
|
||||||
"m/1'/": (c1, K1, cK1),
|
"m/1'/": (c1, K1, cK1),
|
||||||
"m/2'/": (c2, K2, cK2)
|
"m/2'/": (c2, K2, cK2),
|
||||||
|
"m/3'/": (c3, K3, cK3),
|
||||||
|
"m/4'/": (c4, K4, cK4),
|
||||||
|
"m/5'/": (c5, K5, cK5)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.master_private_keys = {
|
self.master_private_keys = {
|
||||||
"m/0'/": k0,
|
"m/0'/": k0,
|
||||||
"m/1'/": k1
|
"m/1'/": k1,
|
||||||
|
"m/2'/": k2,
|
||||||
|
"m/3'/": k3,
|
||||||
|
"m/4'/": k4,
|
||||||
|
"m/5'/": k5
|
||||||
}
|
}
|
||||||
# send k2 to service
|
|
||||||
|
|
||||||
self.config.set_key('master_public_keys', self.master_public_keys, True)
|
self.config.set_key('master_public_keys', self.master_public_keys, True)
|
||||||
self.config.set_key('master_private_keys', self.master_private_keys, True)
|
self.config.set_key('master_private_keys', self.master_private_keys, True)
|
||||||
|
|
||||||
# create default account
|
# create default account
|
||||||
self.create_new_account('Main account', None)
|
self.create_account('Main account')
|
||||||
|
|
||||||
|
|
||||||
def create_new_account(self, name, password):
|
def find_root_by_master_key(self, c, K):
|
||||||
keys = self.accounts.keys()
|
for key, v in self.master_public_keys.items():
|
||||||
i = 0
|
if key == "m/":continue
|
||||||
|
cc, KK, _ = v
|
||||||
|
if (c == cc) and (K == KK):
|
||||||
|
return key
|
||||||
|
|
||||||
while True:
|
def deseed_root(self, seed, password):
|
||||||
derivation = "m/0'/%d'"%i
|
# for safety, we ask the user to enter their seed
|
||||||
if derivation not in keys: break
|
assert seed == self.decode_seed(password)
|
||||||
i += 1
|
self.seed = ''
|
||||||
|
self.config.set_key('seed', '', True)
|
||||||
|
|
||||||
start = "m/0'/"
|
|
||||||
master_k = self.get_master_private_key(start, password )
|
|
||||||
master_c, master_K, master_cK = self.master_public_keys[start]
|
|
||||||
k, c, K, cK = bip32_private_derivation(master_k, master_c, start, derivation)
|
|
||||||
|
|
||||||
self.accounts[derivation] = BIP32_Account({ 'name':name, 'c':c, 'K':K, 'cK':cK })
|
def deseed_branch(self, k):
|
||||||
self.save_accounts()
|
# check that parent has no seed
|
||||||
|
assert self.seed == ''
|
||||||
|
self.master_private_keys.pop(k)
|
||||||
|
self.config.set_key('master_private_keys', self.master_private_keys, True)
|
||||||
|
|
||||||
def create_p2sh_account(self, name):
|
|
||||||
|
def account_id(self, account_type, i):
|
||||||
|
if account_type is None:
|
||||||
|
return "m/0'/%d"%i
|
||||||
|
elif account_type == '2of2':
|
||||||
|
return "m/1'/%d & m/2'/%d"%(i,i)
|
||||||
|
elif account_type == '2of3':
|
||||||
|
return "m/3'/%d & m/4'/%d & m/5'/%d"%(i,i,i)
|
||||||
|
else:
|
||||||
|
raise BaseException('unknown account type')
|
||||||
|
|
||||||
|
|
||||||
|
def num_accounts(self, account_type):
|
||||||
keys = self.accounts.keys()
|
keys = self.accounts.keys()
|
||||||
i = 0
|
i = 0
|
||||||
while True:
|
while True:
|
||||||
account_id = "m/1'/%d & m/2'/%d"%(i,i)
|
account_id = self.account_id(account_type, i)
|
||||||
if account_id not in keys: break
|
if account_id not in keys: break
|
||||||
i += 1
|
i += 1
|
||||||
|
return i
|
||||||
|
|
||||||
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'/"]
|
def create_account(self, name, account_type = None):
|
||||||
c2, K2, cK2 = bip32_public_derivation(master_c2.decode('hex'), master_K2.decode('hex'), "m/2'/", "m/2'/%d"%i)
|
i = self.num_accounts(account_type)
|
||||||
|
account_id = self.account_id(account_type,i)
|
||||||
|
|
||||||
self.accounts[account_id] = BIP32_Account_2of2({ 'name':name, 'c':c1, 'K':K1, 'cK':cK1, 'c2':c2, 'K2':K2, 'cK2':cK2 })
|
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.save_accounts()
|
||||||
|
self.labels[account_id] = name
|
||||||
|
self.config.set_key('labels', self.labels, True)
|
||||||
|
|
||||||
|
|
||||||
def save_accounts(self):
|
def save_accounts(self):
|
||||||
|
@ -283,15 +333,39 @@ class Wallet:
|
||||||
def get_address_index(self, address):
|
def get_address_index(self, address):
|
||||||
if address in self.imported_keys.keys():
|
if address in self.imported_keys.keys():
|
||||||
return -1, None
|
return -1, None
|
||||||
|
|
||||||
for account in self.accounts.keys():
|
for account in self.accounts.keys():
|
||||||
for for_change in [0,1]:
|
for for_change in [0,1]:
|
||||||
addresses = self.accounts[account].get_addresses(for_change)
|
addresses = self.accounts[account].get_addresses(for_change)
|
||||||
for addr in addresses:
|
for addr in addresses:
|
||||||
if address == addr:
|
if address == addr:
|
||||||
return account, (for_change, addresses.index(addr))
|
return account, (for_change, addresses.index(addr))
|
||||||
|
|
||||||
raise BaseException("not found")
|
raise BaseException("not found")
|
||||||
|
|
||||||
|
|
||||||
|
def rebase_sequence(self, account, sequence):
|
||||||
|
c, i = sequence
|
||||||
|
dd = []
|
||||||
|
for a in account.split('&'):
|
||||||
|
s = a.strip()
|
||||||
|
m = re.match("(m/\d+'/)(\d+)", s)
|
||||||
|
root = m.group(1)
|
||||||
|
num = int(m.group(2))
|
||||||
|
dd.append( (root, [num,c,i] ) )
|
||||||
|
return dd
|
||||||
|
|
||||||
|
|
||||||
|
def get_keyID(self, account, sequence):
|
||||||
|
rs = self.rebase_sequence(account, sequence)
|
||||||
|
dd = []
|
||||||
|
for root, public_sequence in rs:
|
||||||
|
c, K, _ = self.master_public_keys[root]
|
||||||
|
s = '/' + '/'.join( map(lambda x:str(x), public_sequence) )
|
||||||
|
dd.append( 'bip32(%s,%s,%s)'%(c,K, s) )
|
||||||
|
return '&'.join(dd)
|
||||||
|
|
||||||
|
|
||||||
def get_public_key(self, address):
|
def get_public_key(self, address):
|
||||||
account, sequence = self.get_address_index(address)
|
account, sequence = self.get_address_index(address)
|
||||||
return self.accounts[account].get_pubkey( *sequence )
|
return self.accounts[account].get_pubkey( *sequence )
|
||||||
|
@ -304,50 +378,37 @@ class Wallet:
|
||||||
|
|
||||||
|
|
||||||
def get_private_key(self, address, password):
|
def get_private_key(self, address, password):
|
||||||
|
out = []
|
||||||
if address in self.imported_keys.keys():
|
if address in self.imported_keys.keys():
|
||||||
return pw_decode( self.imported_keys[address], password )
|
out.append( pw_decode( self.imported_keys[address], password ) )
|
||||||
else:
|
else:
|
||||||
account, sequence = self.get_address_index(address)
|
account, sequence = self.get_address_index(address)
|
||||||
m = re.match("m/0'/(\d+)'", account)
|
# assert address == self.accounts[account].get_address(*sequence)
|
||||||
if m:
|
rs = self.rebase_sequence( account, sequence)
|
||||||
num = int(m.group(1))
|
for root, public_sequence in rs:
|
||||||
master_k = self.get_master_private_key("m/0'/", password)
|
|
||||||
master_c, _, _ = self.master_public_keys["m/0'/"]
|
|
||||||
master_k, master_c = CKD(master_k, master_c, num + BIP32_PRIME)
|
|
||||||
return self.accounts[account].get_private_key(sequence, master_k)
|
|
||||||
|
|
||||||
m2 = re.match("m/1'/(\d+) & m/2'/(\d+)", account)
|
if root not in self.master_private_keys.keys(): continue
|
||||||
if m2:
|
master_k = self.get_master_private_key(root, password)
|
||||||
num = int(m2.group(1))
|
master_c, _, _ = self.master_public_keys[root]
|
||||||
master_k = self.get_master_private_key("m/1'/", password)
|
pk = bip32_private_key( public_sequence, master_k.decode('hex'), master_c.decode('hex'))
|
||||||
master_c, master_K, _ = self.master_public_keys["m/1'/"]
|
out.append(pk)
|
||||||
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
|
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def signrawtransaction(self, tx, input_info, private_keys, password):
|
def signrawtransaction(self, tx, input_info, private_keys, password):
|
||||||
|
import deserialize
|
||||||
unspent_coins = self.get_unspent_coins()
|
unspent_coins = self.get_unspent_coins()
|
||||||
seed = self.decode_seed(password)
|
seed = self.decode_seed(password)
|
||||||
|
|
||||||
# convert private_keys to dict
|
# build a list of public/private keys
|
||||||
pk = {}
|
keypairs = {}
|
||||||
for sec in private_keys:
|
for sec in private_keys:
|
||||||
address = address_from_private_key(sec)
|
pubkey = public_key_from_private_key(sec)
|
||||||
pk[address] = sec
|
keypairs[ pubkey ] = sec
|
||||||
private_keys = pk
|
|
||||||
|
|
||||||
for txin in tx.inputs:
|
for txin in tx.inputs:
|
||||||
# convert to own format
|
# convert to own format
|
||||||
|
@ -363,33 +424,61 @@ class Wallet:
|
||||||
else:
|
else:
|
||||||
for item in unspent_coins:
|
for item in unspent_coins:
|
||||||
if txin['tx_hash'] == item['tx_hash'] and txin['index'] == item['index']:
|
if txin['tx_hash'] == item['tx_hash'] and txin['index'] == item['index']:
|
||||||
|
print_error( "tx input is in unspent coins" )
|
||||||
txin['raw_output_script'] = item['raw_output_script']
|
txin['raw_output_script'] = item['raw_output_script']
|
||||||
|
account, sequence = self.get_address_index(item['address'])
|
||||||
|
if account != -1:
|
||||||
|
txin['redeemScript'] = self.accounts[account].redeem_script(sequence)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
# if neither, we might want to get it from the server..
|
raise BaseException("Unknown transaction input. Please provide the 'input_info' parameter, or synchronize this wallet")
|
||||||
raise
|
|
||||||
|
|
||||||
# find the address:
|
# if available, derive private_keys from KeyID
|
||||||
if txin.get('KeyID'):
|
keyid = txin.get('KeyID')
|
||||||
account, name, sequence = txin.get('KeyID')
|
if keyid:
|
||||||
if name != 'Electrum': continue
|
roots = []
|
||||||
sec = self.accounts[account].get_private_key(sequence, seed)
|
for s in keyid.split('&'):
|
||||||
addr = self.accounts[account].get_address(sequence)
|
m = re.match("bip32\(([0-9a-f]+),([0-9a-f]+),(/\d+/\d+/\d+)", s)
|
||||||
|
if not m: continue
|
||||||
|
c = m.group(1)
|
||||||
|
K = m.group(2)
|
||||||
|
sequence = m.group(3)
|
||||||
|
root = self.find_root_by_master_key(c,K)
|
||||||
|
if not root: continue
|
||||||
|
sequence = map(lambda x:int(x), sequence.strip('/').split('/'))
|
||||||
|
root = root + '%d'%sequence[0]
|
||||||
|
sequence = sequence[1:]
|
||||||
|
roots.append((root,sequence))
|
||||||
|
|
||||||
|
account_id = " & ".join( map(lambda x:x[0], roots) )
|
||||||
|
account = self.accounts.get(account_id)
|
||||||
|
if not account: continue
|
||||||
|
addr = account.get_address(*sequence)
|
||||||
txin['address'] = addr
|
txin['address'] = addr
|
||||||
private_keys[addr] = sec
|
pk = self.get_private_key(addr, password)
|
||||||
|
for sec in pk:
|
||||||
|
pubkey = public_key_from_private_key(sec)
|
||||||
|
keypairs[pubkey] = sec
|
||||||
|
|
||||||
elif txin.get("redeemScript"):
|
redeem_script = txin.get("redeemScript")
|
||||||
txin['address'] = hash_160_to_bc_address(hash_160(txin.get("redeemScript").decode('hex')), 5)
|
print_error( "p2sh:", "yes" if redeem_script else "no")
|
||||||
|
if redeem_script:
|
||||||
|
addr = hash_160_to_bc_address(hash_160(redeem_script.decode('hex')), 5)
|
||||||
|
else:
|
||||||
|
addr = deserialize.get_address_from_output_script(txin["raw_output_script"].decode('hex'))
|
||||||
|
txin['address'] = addr
|
||||||
|
|
||||||
elif txin.get("raw_output_script"):
|
# add private keys that are in the wallet
|
||||||
import deserialize
|
pk = self.get_private_key(addr, password)
|
||||||
addr = deserialize.get_address_from_output_script(txin.get("raw_output_script").decode('hex'))
|
for sec in pk:
|
||||||
sec = self.get_private_key(addr, password)
|
pubkey = public_key_from_private_key(sec)
|
||||||
if sec:
|
keypairs[pubkey] = sec
|
||||||
private_keys[addr] = sec
|
if not redeem_script:
|
||||||
txin['address'] = addr
|
txin['redeemPubkey'] = pubkey
|
||||||
|
|
||||||
tx.sign( private_keys )
|
print txin
|
||||||
|
|
||||||
|
tx.sign( keypairs )
|
||||||
|
|
||||||
def sign_message(self, address, message, password):
|
def sign_message(self, address, message, password):
|
||||||
sec = self.get_private_key(address, password)
|
sec = self.get_private_key(address, password)
|
||||||
|
@ -513,7 +602,7 @@ class Wallet:
|
||||||
self.config.set_key('contacts', self.addressbook, True)
|
self.config.set_key('contacts', self.addressbook, True)
|
||||||
if label:
|
if label:
|
||||||
self.labels[address] = label
|
self.labels[address] = label
|
||||||
self.config.set_key('labels', self.labels)
|
self.config.set_key('labels', self.labels, True)
|
||||||
|
|
||||||
def delete_contact(self, addr):
|
def delete_contact(self, addr):
|
||||||
if addr in self.addressbook:
|
if addr in self.addressbook:
|
||||||
|
@ -606,7 +695,7 @@ class Wallet:
|
||||||
def get_accounts(self):
|
def get_accounts(self):
|
||||||
accounts = {}
|
accounts = {}
|
||||||
for k, account in self.accounts.items():
|
for k, account in self.accounts.items():
|
||||||
accounts[k] = account.name
|
accounts[k] = self.labels.get(k, 'unnamed')
|
||||||
if self.imported_keys:
|
if self.imported_keys:
|
||||||
accounts[-1] = 'Imported keys'
|
accounts[-1] = 'Imported keys'
|
||||||
return accounts
|
return accounts
|
||||||
|
@ -873,13 +962,6 @@ class Wallet:
|
||||||
|
|
||||||
|
|
||||||
def mktx(self, outputs, password, fee=None, change_addr=None, account=None ):
|
def mktx(self, outputs, password, fee=None, change_addr=None, account=None ):
|
||||||
"""
|
|
||||||
create a transaction
|
|
||||||
account parameter:
|
|
||||||
None means use all accounts
|
|
||||||
-1 means imported keys
|
|
||||||
0, 1, etc are seed accounts
|
|
||||||
"""
|
|
||||||
|
|
||||||
for address, x in outputs:
|
for address, x in outputs:
|
||||||
assert is_valid(address)
|
assert is_valid(address)
|
||||||
|
@ -891,33 +973,28 @@ class Wallet:
|
||||||
raise ValueError("Not enough funds")
|
raise ValueError("Not enough funds")
|
||||||
|
|
||||||
outputs = self.add_tx_change(inputs, outputs, amount, fee, total, change_addr, account)
|
outputs = self.add_tx_change(inputs, outputs, amount, fee, total, change_addr, account)
|
||||||
|
|
||||||
tx = Transaction.from_io(inputs, outputs)
|
tx = Transaction.from_io(inputs, outputs)
|
||||||
|
|
||||||
pk_addresses = []
|
keypairs = {}
|
||||||
for i in range(len(tx.inputs)):
|
for i, txin in enumerate(tx.inputs):
|
||||||
txin = tx.inputs[i]
|
|
||||||
address = txin['address']
|
address = txin['address']
|
||||||
if address in self.imported_keys.keys():
|
|
||||||
pk_addresses.append(address)
|
|
||||||
continue
|
|
||||||
account, sequence = self.get_address_index(address)
|
account, sequence = self.get_address_index(address)
|
||||||
|
txin['KeyID'] = self.get_keyID(account, sequence)
|
||||||
|
|
||||||
txin['KeyID'] = (account, 'BIP32', sequence) # used by the server to find the key
|
redeemScript = self.accounts[account].redeem_script(sequence)
|
||||||
|
if redeemScript:
|
||||||
|
txin['redeemScript'] = redeemScript
|
||||||
|
else:
|
||||||
|
txin['redeemPubkey'] = self.accounts[account].get_pubkey(*sequence)
|
||||||
|
|
||||||
_, redeemScript = self.accounts[account].get_input_info(sequence)
|
private_keys = self.get_private_key(address, password)
|
||||||
|
|
||||||
if redeemScript: txin['redeemScript'] = redeemScript
|
for sec in private_keys:
|
||||||
pk_addresses.append(address)
|
pubkey = public_key_from_private_key(sec)
|
||||||
|
keypairs[ pubkey ] = sec
|
||||||
print "pk_addresses", pk_addresses
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
|
tx.sign(keypairs)
|
||||||
for address, x in outputs:
|
for address, x in outputs:
|
||||||
if address not in self.addressbook and not self.is_mine(address):
|
if address not in self.addressbook and not self.is_mine(address):
|
||||||
self.addressbook.append(address)
|
self.addressbook.append(address)
|
||||||
|
|
Loading…
Reference in New Issue