Compare commits

...

40 Commits

Author SHA1 Message Date
ThomasV
82e88cb89d prepare release 3.0.6 2018-02-03 07:38:36 +01:00
SomberNight
d76f36df70 ledger: mention "bitcoin" app when update is needed 2018-02-02 02:41:41 +01:00
SomberNight
cf82b1d9d5 follow-up 70aa1f1db9 2018-01-31 07:05:29 +01:00
SomberNight
29206852fb follow-up prev commit. better handling of p2sh-segwit. added comment to describe the problem for native segwit. 2018-01-31 07:05:13 +01:00
Neil Booth
f0942a2535 Fix PNG file
Avoids libpng warning: iCCP: known incorrect sRGB profile
2018-01-31 07:04:57 +01:00
SomberNight
7b8b75c7d3 fix #3788
# Conflicts:
#	lib/transaction.py
2018-01-31 07:04:16 +01:00
SomberNight
b0966671d7 fix #3790 2018-01-31 07:02:46 +01:00
SomberNight
64fa7dd6c3 fix #3783 2018-01-31 07:02:31 +01:00
Johann Bauer
d0433c1539 Change number of zero when base unit changes 2018-01-29 20:57:23 +01:00
SomberNight
ff38e90405 revert adding handling of 'blockchain.address.subscribe' in network.py 2018-01-29 20:57:11 +01:00
SomberNight
c79662fbce websocket: migrate to scripthashes 2018-01-29 20:56:59 +01:00
racquemis
d5e20d607e Handle invalid PIN on exporting private key
Prevent Android App from crashing when a wrong PIN is entered.
2018-01-29 20:56:46 +01:00
SomberNight
53fc343b4a setconfig rpcpassword: don't try to evaluate or reencode 2018-01-29 20:56:30 +01:00
SomberNight
70d6f50d94 fix-up stdio gui 2018-01-29 20:56:16 +01:00
ThomasV
1c4773d41a fix #3619: set correct parent window 2018-01-29 20:56:03 +01:00
SomberNight
7cfa3c2d51 ledger: handle pin-locked state better 2018-01-29 20:55:49 +01:00
Marcel O'Neil
47f57af145 fix crash when exporting private keys 2018-01-29 20:55:35 +01:00
SomberNight
7279fc8902 qt privkey export: if multisig wallet, warn users re backup 2018-01-29 20:54:10 +01:00
SomberNight
87737dbe45 fix #3663: 'copying' QR code does not save to file 2018-01-29 20:53:47 +01:00
SomberNight
231f4931d6 avoid shorter seeds 'by luck' 2018-01-29 20:53:19 +01:00
ThomasV
a9973ce6ab version 3.0.5 2018-01-08 00:50:15 +01:00
SomberNight
1fc45132c0 Password-protect the JSON RPC interface 2018-01-08 00:29:57 +01:00
ThomasV
e65353c062 disable jsonrpc commands in the GUI 2018-01-07 23:53:25 +01:00
ThomasV
0045784a58 disable jsonrpc on android 2018-01-07 16:13:56 +01:00
ThomasV
79d402d3f9 update hw wallet versions 2018-01-07 15:13:59 +01:00
ThomasV
063ec0a758 release 3.0.4 2018-01-06 22:45:41 +01:00
Johann Bauer
d4f1445914 Bundle QR scanner with Android app 2018-01-06 22:44:37 +01:00
SomberNight
498a269c88 fix #3411 2018-01-06 22:44:12 +01:00
Andrew Chow
b7c20e71ac Avoid modifying self.transactions in prepare_for_verifier
In python3, the `.keys()` function returns an iterator, not a list,
so to get a list that can be iterated over, use `list()` instead to
avoid modification of a list while in use.
2018-01-06 22:44:04 +01:00
SomberNight
a6e59499db fix #3217: make sure qt quits 2018-01-06 22:43:56 +01:00
SomberNight
d3a963e673 fix kivy refresh bug with Addresses tab 2018-01-06 22:43:49 +01:00
SomberNight
e4308a360b fix #3601 2018-01-06 22:43:09 +01:00
SomberNight
e98406fc7c fix: kivy Addresses tab crash for Imported_Wallet change 2018-01-06 22:43:04 +01:00
SomberNight
dfaf4817c9 fix #3578 2018-01-06 22:42:55 +01:00
SomberNight
0f54051ecb fix: address filters for Imported_Wallet: wallet.is_used() 2018-01-06 22:42:46 +01:00
SomberNight
02fda5a85b fix: crash when closing qrscanner window (see #3546) 2018-01-06 22:42:37 +01:00
Charles Bell
d9925967b7 Fix segfault when using a specific camera device 2018-01-06 22:42:29 +01:00
SomberNight
006aece3a3 fix #3526 2018-01-06 22:42:22 +01:00
ThomasV
c7a47a06b5 format a few strings with str.format(). fix #3405 2018-01-06 22:42:15 +01:00
Tristan Seligmann
fdd10bfb60 Stop allowing CORS for the JSON-RPC server
As far as I can tell, there is no need to allow this, and doing so poses severe security risks (see #3374).
2018-01-06 22:41:58 +01:00
33 changed files with 462 additions and 154 deletions

View File

@ -1,3 +1,28 @@
# Release 3.0.6 :
* Fix transaction parsing bug #3788
# Release 3.0.5 : (Security update)
This is a follow-up to the 3.0.4 release, which did not completely fix
issue #3374. Users should upgrade to 3.0.5.
* The JSONRPC interface is password protected
* JSONRPC commands are disabled if the GUI is running, except 'ping',
which is used to determine if a GUI is already running
# Release 3.0.4 : (Security update)
* Fix a vulnerability caused by Cross-Origin Resource Sharing (CORS)
in the JSONRPC interface. Previous versions of Electrum are
vulnerable to port scanning and deanonimization attacks from
malicious websites. Wallets that are not password-protected are
vulnerable to theft.
* Bundle QR scanner with Android app
* Minor bug fixes
# Release 3.0.3 # Release 3.0.3
* Qt GUI: sweeping now uses the Send tab, allowing fees to be set * Qt GUI: sweeping now uses the Send tab, allowing fees to be set
* Windows: if using the installer binary, there is now a separate shortcut * Windows: if using the installer binary, there is now a separate shortcut

View File

@ -43,6 +43,7 @@ done
popd popd
pushd electrum pushd electrum
git checkout $BRANCH
VERSION=`git describe --tags` VERSION=`git describe --tags`
echo "Last commit: $VERSION" echo "Last commit: $VERSION"
find -exec touch -d '2000-11-11T11:11:11+00:00' {} + find -exec touch -d '2000-11-11T11:11:11+00:00' {} +

View File

@ -23,6 +23,6 @@ cd tmp
$PYTHON -m pip install setuptools --upgrade $PYTHON -m pip install setuptools --upgrade
$PYTHON -m pip install cython --upgrade $PYTHON -m pip install cython --upgrade
$PYTHON -m pip install trezor==0.7.16 --upgrade $PYTHON -m pip install trezor==0.7.16 --upgrade
$PYTHON -m pip install keepkey==4.0.0 --upgrade $PYTHON -m pip install keepkey==4.0.2 --upgrade
$PYTHON -m pip install btchip-python==0.1.23 --upgrade $PYTHON -m pip install btchip-python==0.1.24 --upgrade

View File

@ -278,7 +278,8 @@ def run_offline_command(config, config_options):
# arguments passed to function # arguments passed to function
args = [config.get(x) for x in cmd.params] args = [config.get(x) for x in cmd.params]
# decode json arguments # decode json arguments
args = list(map(json_decode, args)) if cmdname not in ('setconfig',):
args = list(map(json_decode, args))
# options # options
kwargs = {} kwargs = {}
for x in cmd.options: for x in cmd.options:
@ -372,7 +373,7 @@ if __name__ == '__main__':
fd, server = daemon.get_fd_or_server(config) fd, server = daemon.get_fd_or_server(config)
if fd is not None: if fd is not None:
plugins = init_plugins(config, config.get('gui', 'qt')) plugins = init_plugins(config, config.get('gui', 'qt'))
d = daemon.Daemon(config, fd) d = daemon.Daemon(config, fd, True)
d.start() d.start()
d.init_gui(config, plugins) d.init_gui(config, plugins)
sys.exit(0) sys.exit(0)
@ -393,7 +394,7 @@ if __name__ == '__main__':
print_stderr("starting daemon (PID %d)" % pid) print_stderr("starting daemon (PID %d)" % pid)
sys.exit(0) sys.exit(0)
init_plugins(config, 'cmdline') init_plugins(config, 'cmdline')
d = daemon.Daemon(config, fd) d = daemon.Daemon(config, fd, False)
d.start() d.start()
if config.get('websocket_server'): if config.get('websocket_server'):
from electrum import websockets from electrum import websockets
@ -425,7 +426,6 @@ if __name__ == '__main__':
else: else:
init_plugins(config, 'cmdline') init_plugins(config, 'cmdline')
result = run_offline_command(config, config_options) result = run_offline_command(config, config_options)
# print result # print result
if isinstance(result, str): if isinstance(result, str):
print_msg(result) print_msg(result)

View File

@ -0,0 +1,48 @@
package org.electrum.qr;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.content.Intent;
import java.util.Arrays;
import me.dm7.barcodescanner.zxing.ZXingScannerView;
import com.google.zxing.Result;
import com.google.zxing.BarcodeFormat;
public class SimpleScannerActivity extends Activity implements ZXingScannerView.ResultHandler {
private ZXingScannerView mScannerView;
final String TAG = "org.electrum.SimpleScannerActivity";
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
mScannerView = new ZXingScannerView(this); // Programmatically initialize the scanner view
mScannerView.setFormats(Arrays.asList(BarcodeFormat.QR_CODE));
setContentView(mScannerView); // Set the scanner view as the content view
}
@Override
public void onResume() {
super.onResume();
mScannerView.setResultHandler(this); // Register ourselves as a handler for scan results.
mScannerView.startCamera(); // Start camera on resume
}
@Override
public void onPause() {
super.onPause();
mScannerView.stopCamera(); // Stop camera on pause
}
@Override
public void handleResult(Result rawResult) {
Intent resultIntent = new Intent();
resultIntent.putExtra("text", rawResult.getText());
resultIntent.putExtra("format", rawResult.getBarcodeFormat().toString());
setResult(Activity.RESULT_OK, resultIntent);
this.finish();
}
}

View File

@ -325,7 +325,7 @@ class ElectrumWindow(App):
@profiler @profiler
def update_tabs(self): def update_tabs(self):
for tab in ['invoices', 'send', 'history', 'receive', 'requests']: for tab in ['invoices', 'send', 'history', 'receive', 'address']:
self.update_tab(tab) self.update_tab(tab)
def switch_to(self, name): def switch_to(self, name):
@ -384,45 +384,22 @@ class ElectrumWindow(App):
def scan_qr(self, on_complete): def scan_qr(self, on_complete):
if platform != 'android': if platform != 'android':
return return
from jnius import autoclass from jnius import autoclass, cast
from android import activity from android import activity
PythonActivity = autoclass('org.kivy.android.PythonActivity') PythonActivity = autoclass('org.kivy.android.PythonActivity')
SimpleScannerActivity = autoclass("org.electrum.qr.SimpleScannerActivity")
Intent = autoclass('android.content.Intent') Intent = autoclass('android.content.Intent')
intent = Intent("com.google.zxing.client.android.SCAN") intent = Intent(PythonActivity.mActivity, SimpleScannerActivity)
intent.putExtra("SCAN_MODE", "QR_CODE_MODE")
def on_qr_result(requestCode, resultCode, intent):
if requestCode == 0:
if resultCode == -1: # RESULT_OK:
contents = intent.getStringExtra("SCAN_RESULT")
if intent.getStringExtra("SCAN_RESULT_FORMAT") == 'QR_CODE':
on_complete(contents)
else:
self.show_error("wrong format " + intent.getStringExtra("SCAN_RESULT_FORMAT"))
activity.bind(on_activity_result=on_qr_result)
try:
PythonActivity.mActivity.startActivityForResult(intent, 0)
except:
self.show_error(_('Could not start Barcode Scanner.') + ' ' + _('Please install the Barcode Scanner app from ZXing'))
def scan_qr_zxing(self, on_complete):
# uses zxing embedded lib
if platform != 'android':
return
from jnius import autoclass
from android import activity
PythonActivity = autoclass('org.kivy.android.PythonActivity')
IntentIntegrator = autoclass('com.google.zxing.integration.android.IntentIntegrator')
integrator = IntentIntegrator(PythonActivity.mActivity)
def on_qr_result(requestCode, resultCode, intent): def on_qr_result(requestCode, resultCode, intent):
if requestCode == 0: if resultCode == -1: # RESULT_OK:
if resultCode == -1: # RESULT_OK: # this doesn't work due to some bug in jnius:
contents = intent.getStringExtra("SCAN_RESULT") # contents = intent.getStringExtra("text")
if intent.getStringExtra("SCAN_RESULT_FORMAT") == 'QR_CODE': String = autoclass("java.lang.String")
on_complete(contents) contents = intent.getStringExtra(String("text"))
else: on_complete(contents)
self.show_error("wrong format " + intent.getStringExtra("SCAN_RESULT_FORMAT"))
activity.bind(on_activity_result=on_qr_result) activity.bind(on_activity_result=on_qr_result)
integrator.initiateScan() PythonActivity.mActivity.startActivityForResult(intent, 0)
def do_share(self, data, title): def do_share(self, data, title):
if platform != 'android': if platform != 'android':
@ -943,9 +920,18 @@ class ElectrumWindow(App):
self._password_dialog.open() self._password_dialog.open()
def export_private_keys(self, pk_label, addr): def export_private_keys(self, pk_label, addr):
if self.wallet.is_watching_only():
self.show_info(_('This is a watching-only wallet. It does not contain private keys.'))
return
def show_private_key(addr, pk_label, password): def show_private_key(addr, pk_label, password):
if self.wallet.has_password() and password is None: if self.wallet.has_password() and password is None:
return return
key = str(self.wallet.export_private_key(addr, password)[0]) if not self.wallet.can_export():
pk_label.data = key return
try:
key = str(self.wallet.export_private_key(addr, password)[0])
pk_label.data = key
except InvalidPassword:
self.show_error("Invalid PIN")
return
self.protected(_("Enter your PIN code in order to decrypt your private key"), show_private_key, (addr, pk_label)) self.protected(_("Enter your PIN code in order to decrypt your private key"), show_private_key, (addr, pk_label))

View File

@ -52,7 +52,8 @@ fullscreen = False
# #
# (list) Permissions # (list) Permissions
android.permissions = INTERNET, WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE android.permissions = INTERNET, WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE, CAMERA
# (int) Android API to use # (int) Android API to use
#android.api = 14 #android.api = 14
@ -86,7 +87,11 @@ android.ndk_path = /opt/crystax-ndk-10.3.2
# (list) List of Java files to add to the android project (can be java or a # (list) List of Java files to add to the android project (can be java or a
# directory containing the files) # directory containing the files)
#android.add_src = android.add_src = gui/kivy/data/java-classes/
android.gradle_dependencies = me.dm7.barcodescanner:zxing:1.9.8
android.add_activities = org.electrum.qr.SimpleScannerActivity
# (str) python-for-android branch to use, if not master, useful to try # (str) python-for-android branch to use, if not master, useful to try
# not yet merged features. # not yet merged features.

View File

@ -521,7 +521,7 @@ class AddressScreen(CScreen):
def update(self): def update(self):
self.menu_actions = [('Receive', self.do_show), ('Details', self.do_view)] self.menu_actions = [('Receive', self.do_show), ('Details', self.do_view)]
wallet = self.app.wallet wallet = self.app.wallet
_list = wallet.change_addresses if self.screen.show_change else wallet.receiving_addresses _list = wallet.get_change_addresses() if self.screen.show_change else wallet.get_receiving_addresses()
search = self.screen.message search = self.screen.message
container = self.screen.ids.search_container container = self.screen.ids.search_container
container.clear_widgets() container.clear_widgets()

View File

@ -240,6 +240,7 @@ class ElectrumGui:
except GoBack: except GoBack:
return return
except: except:
import traceback
traceback.print_exc(file=sys.stdout) traceback.print_exc(file=sys.stdout)
return return
self.timer.start() self.timer.start()
@ -248,11 +249,23 @@ class ElectrumGui:
if not self.start_new_window(path, self.config.get('url')): if not self.start_new_window(path, self.config.get('url')):
return return
signal.signal(signal.SIGINT, lambda *args: self.app.quit()) signal.signal(signal.SIGINT, lambda *args: self.app.quit())
def quit_after_last_window():
# on some platforms, not only does exec_ not return but not even
# aboutToQuit is emitted (but following this, it should be emitted)
if self.app.quitOnLastWindowClosed():
self.app.quit()
self.app.lastWindowClosed.connect(quit_after_last_window)
def clean_up():
# Shut down the timer cleanly
self.timer.stop()
# clipboard persistence. see http://www.mail-archive.com/pyqt@riverbankcomputing.com/msg17328.html
event = QtCore.QEvent(QtCore.QEvent.Clipboard)
self.app.sendEvent(self.app.clipboard(), event)
self.tray.hide()
self.app.aboutToQuit.connect(clean_up)
# main loop # main loop
self.app.exec_() self.app.exec_()
# Shut down the timer cleanly # on some platforms the exec_ call may not return, so use clean_up()
self.timer.stop()
# clipboard persistence. see http://www.mail-archive.com/pyqt@riverbankcomputing.com/msg17328.html
event = QtCore.QEvent(QtCore.QEvent.Clipboard)
self.app.sendEvent(self.app.clipboard(), event)
self.tray.hide()

View File

@ -93,6 +93,6 @@ class AddressDialog(WindowModalDialog):
def show_qr(self): def show_qr(self):
text = self.address text = self.address
try: try:
self.parent.show_qrcode(text, 'Address') self.parent.show_qrcode(text, 'Address', parent=self)
except Exception as e: except Exception as e:
self.show_message(str(e)) self.show_message(str(e))

View File

@ -527,10 +527,10 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
grid.addWidget(m_label, 1, 0) grid.addWidget(m_label, 1, 0)
grid.addWidget(m_edit, 1, 1) grid.addWidget(m_edit, 1, 1)
def on_m(m): def on_m(m):
m_label.setText(_('Require %d signatures')%m) m_label.setText(_('Require {0} signatures').format(m))
cw.set_m(m) cw.set_m(m)
def on_n(n): def on_n(n):
n_label.setText(_('From %d cosigners')%n) n_label.setText(_('From {0} cosigners').format(n))
cw.set_n(n) cw.set_n(n)
m_edit.setMaximum(n) m_edit.setMaximum(n)
n_edit.valueChanged.connect(on_n) n_edit.valueChanged.connect(on_n)

View File

@ -2164,6 +2164,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.show_message(_("This is a watching-only wallet")) self.show_message(_("This is a watching-only wallet"))
return return
if isinstance(self.wallet, Multisig_Wallet):
self.show_message(_('WARNING: This is a multi-signature wallet.') + '\n' +
_('It can not be "backed up" by simply exporting these private keys.'))
d = WindowModalDialog(self, _('Private keys')) d = WindowModalDialog(self, _('Private keys'))
d.setMinimumSize(850, 300) d.setMinimumSize(850, 300)
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
@ -2189,25 +2193,38 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
private_keys = {} private_keys = {}
addresses = self.wallet.get_addresses() addresses = self.wallet.get_addresses()
done = False done = False
cancelled = False
def privkeys_thread(): def privkeys_thread():
for addr in addresses: for addr in addresses:
time.sleep(0.1) time.sleep(0.1)
if done: if done or cancelled:
break break
privkey = self.wallet.export_private_key(addr, password)[0] privkey = self.wallet.export_private_key(addr, password)[0]
private_keys[addr] = privkey private_keys[addr] = privkey
self.computing_privkeys_signal.emit() self.computing_privkeys_signal.emit()
self.computing_privkeys_signal.disconnect() if not cancelled:
self.show_privkeys_signal.emit() self.computing_privkeys_signal.disconnect()
self.show_privkeys_signal.emit()
def show_privkeys(): def show_privkeys():
s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items())) s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
e.setText(s) e.setText(s)
b.setEnabled(True) b.setEnabled(True)
self.show_privkeys_signal.disconnect() self.show_privkeys_signal.disconnect()
nonlocal done
done = True
def on_dialog_closed(*args):
nonlocal done
nonlocal cancelled
if not done:
cancelled = True
self.computing_privkeys_signal.disconnect()
self.show_privkeys_signal.disconnect()
self.computing_privkeys_signal.connect(lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses)))) self.computing_privkeys_signal.connect(lambda: e.setText("Please wait... %d/%d"%(len(private_keys),len(addresses))))
self.show_privkeys_signal.connect(show_privkeys) self.show_privkeys_signal.connect(show_privkeys)
d.finished.connect(on_dialog_closed)
threading.Thread(target=privkeys_thread).start() threading.Thread(target=privkeys_thread).start()
if not d.exec_(): if not d.exec_():
@ -2437,6 +2454,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.fiat_receive_e.setVisible(b) self.fiat_receive_e.setVisible(b)
self.history_list.refresh_headers() self.history_list.refresh_headers()
self.history_list.update() self.history_list.update()
self.address_list.refresh_headers()
self.address_list.update() self.address_list.update()
self.update_status() self.update_status()
@ -2587,7 +2605,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
unit_combo = QComboBox() unit_combo = QComboBox()
unit_combo.addItems(units) unit_combo.addItems(units)
unit_combo.setCurrentIndex(units.index(self.base_unit())) unit_combo.setCurrentIndex(units.index(self.base_unit()))
def on_unit(x): def on_unit(x, nz):
unit_result = units[unit_combo.currentIndex()] unit_result = units[unit_combo.currentIndex()]
if self.base_unit() == unit_result: if self.base_unit() == unit_result:
return return
@ -2602,13 +2620,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
else: else:
raise Exception('Unknown base unit') raise Exception('Unknown base unit')
self.config.set_key('decimal_point', self.decimal_point, True) self.config.set_key('decimal_point', self.decimal_point, True)
nz.setMaximum(self.decimal_point)
self.history_list.update() self.history_list.update()
self.request_list.update() self.request_list.update()
self.address_list.update() self.address_list.update()
for edit, amount in zip(edits, amounts): for edit, amount in zip(edits, amounts):
edit.setAmount(amount) edit.setAmount(amount)
self.update_status() self.update_status()
unit_combo.currentIndexChanged.connect(on_unit) unit_combo.currentIndexChanged.connect(lambda x: on_unit(x, nz))
gui_widgets.append((unit_label, unit_combo)) gui_widgets.append((unit_label, unit_combo))
block_explorers = sorted(util.block_explorer_info().keys()) block_explorers = sorted(util.block_explorer_info().keys())

View File

@ -355,14 +355,14 @@ class NetworkChoiceLayout(object):
height_str = "%d "%(self.network.get_local_height()) + _('blocks') height_str = "%d "%(self.network.get_local_height()) + _('blocks')
self.height_label.setText(height_str) self.height_label.setText(height_str)
n = len(self.network.get_interfaces()) n = len(self.network.get_interfaces())
status = _("Connected to %d nodes.")%n if n else _("Not connected") status = _("Connected to {0} nodes.").format(n) if n else _("Not connected")
self.status_label.setText(status) self.status_label.setText(status)
chains = self.network.get_blockchains() chains = self.network.get_blockchains()
if len(chains)>1: if len(chains)>1:
chain = self.network.blockchain() chain = self.network.blockchain()
checkpoint = chain.get_checkpoint() checkpoint = chain.get_checkpoint()
name = chain.get_name() name = chain.get_name()
msg = _('Chain split detected at block %d')%checkpoint + '\n' msg = _('Chain split detected at block {0}').format(checkpoint) + '\n'
msg += (_('You are following branch') if auto_connect else _('Your server is on branch'))+ ' ' + name msg += (_('You are following branch') if auto_connect else _('Your server is on branch'))+ ' ' + name
msg += ' (%d %s)' % (chain.get_branch_size(), _('blocks')) msg += ' (%d %s)' % (chain.get_branch_size(), _('blocks'))
else: else:

View File

@ -113,8 +113,7 @@ class QRDialog(WindowModalDialog):
def copy_to_clipboard(): def copy_to_clipboard():
p = qscreen.grabWindow(qrw.winId()) p = qscreen.grabWindow(qrw.winId())
p.save(filename, 'png') QApplication.clipboard().setPixmap(p)
QApplication.clipboard().setImage(QImage(filename))
self.show_message(_("QR code copied to clipboard")) self.show_message(_("QR code copied to clipboard"))
b = QPushButton(_("Copy")) b = QPushButton(_("Copy"))

View File

@ -56,6 +56,8 @@ class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin):
except BaseException as e: except BaseException as e:
self.show_error(str(e)) self.show_error(str(e))
data = '' data = ''
if not data:
data = ''
self.setText(data) self.setText(data)
return data return data

View File

@ -35,7 +35,7 @@ from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit
def seed_warning_msg(seed): def seed_warning_msg(seed):
return ''.join([ return ''.join([
"<p>", "<p>",
_("Please save these %d words on paper (order is important). "), _("Please save these {0} words on paper (order is important). "),
_("This seed will allow you to recover your wallet in case " _("This seed will allow you to recover your wallet in case "
"of computer failure."), "of computer failure."),
"</p>", "</p>",
@ -45,7 +45,7 @@ def seed_warning_msg(seed):
"<li>" + _("Never type it on a website.") + "</li>", "<li>" + _("Never type it on a website.") + "</li>",
"<li>" + _("Do not store it electronically.") + "</li>", "<li>" + _("Do not store it electronically.") + "</li>",
"</ul>" "</ul>"
]) % len(seed.split()) ]).format(len(seed.split()))
class SeedLayout(QVBoxLayout): class SeedLayout(QVBoxLayout):

View File

@ -85,12 +85,11 @@ class ElectrumGui:
delta = (80 - sum(width) - 4)/3 delta = (80 - sum(width) - 4)/3
format_str = "%"+"%d"%width[0]+"s"+"%"+"%d"%(width[1]+delta)+"s"+"%" \ format_str = "%"+"%d"%width[0]+"s"+"%"+"%d"%(width[1]+delta)+"s"+"%" \
+ "%d"%(width[2]+delta)+"s"+"%"+"%d"%(width[3]+delta)+"s" + "%d"%(width[2]+delta)+"s"+"%"+"%d"%(width[3]+delta)+"s"
b = 0
messages = [] messages = []
for item in self.wallet.get_history(): for item in self.wallet.get_history():
tx_hash, confirmations, value, timestamp, balance = item tx_hash, height, conf, timestamp, delta, balance = item
if confirmations: if conf:
try: try:
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
except Exception: except Exception:
@ -99,7 +98,7 @@ class ElectrumGui:
time_str = 'unconfirmed' time_str = 'unconfirmed'
label = self.wallet.get_label(tx_hash) label = self.wallet.get_label(tx_hash)
messages.append( format_str%( time_str, label, format_satoshis(value, whitespaces=True), format_satoshis(balance, whitespaces=True) ) ) messages.append( format_str%( time_str, label, format_satoshis(delta, whitespaces=True), format_satoshis(balance, whitespaces=True) ) )
self.print_list(messages[::-1], format_str%( _("Date"), _("Description"), _("Amount"), _("Balance"))) self.print_list(messages[::-1], format_str%( _("Date"), _("Description"), _("Amount"), _("Balance")))
@ -149,12 +148,13 @@ class ElectrumGui:
for i, x in enumerate( self.wallet.network.banner.split('\n') ): for i, x in enumerate( self.wallet.network.banner.split('\n') ):
print( x ) print( x )
def print_list(self, list, firstline): def print_list(self, lst, firstline):
self.maxpos = len(list) lst = list(lst)
self.maxpos = len(lst)
if not self.maxpos: return if not self.maxpos: return
print(firstline) print(firstline)
for i in range(self.maxpos): for i in range(self.maxpos):
msg = list[i] if i < len(list) else "" msg = lst[i] if i < len(lst) else ""
print(msg) print(msg)
@ -176,7 +176,7 @@ class ElectrumGui:
print(_('Invalid Fee')) print(_('Invalid Fee'))
return return
if self.wallet.use_encryption: if self.wallet.has_password():
password = self.password_dialog() password = self.password_dialog()
if not password: if not password:
return return

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -34,7 +34,7 @@ from functools import wraps
from decimal import Decimal from decimal import Decimal
from .import util from .import util
from .util import bfh, bh2u, format_satoshis from .util import bfh, bh2u, format_satoshis, json_decode
from .import bitcoin from .import bitcoin
from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS
from .i18n import _ from .i18n import _
@ -151,10 +151,8 @@ class Commands:
@command('') @command('')
def setconfig(self, key, value): def setconfig(self, key, value):
"""Set a configuration variable. 'value' may be a string or a Python expression.""" """Set a configuration variable. 'value' may be a string or a Python expression."""
try: if key not in ('rpcuser', 'rpcpassword'):
value = ast.literal_eval(value) value = json_decode(value)
except:
pass
self.config.set_key(key, value) self.config.set_key(key, value)
return True return True

View File

@ -28,12 +28,12 @@ import time
# from jsonrpc import JSONRPCResponseManager # from jsonrpc import JSONRPCResponseManager
import jsonrpclib import jsonrpclib
from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer, SimpleJSONRPCRequestHandler from .jsonrpc import VerifyingJSONRPCServer
from .version import ELECTRUM_VERSION from .version import ELECTRUM_VERSION
from .network import Network from .network import Network
from .util import json_decode, DaemonThread from .util import json_decode, DaemonThread
from .util import print_error from .util import print_error, to_string
from .wallet import Wallet from .wallet import Wallet
from .storage import WalletStorage from .storage import WalletStorage
from .commands import known_commands, Commands from .commands import known_commands, Commands
@ -75,7 +75,14 @@ def get_server(config):
try: try:
with open(lockfile) as f: with open(lockfile) as f:
(host, port), create_time = ast.literal_eval(f.read()) (host, port), create_time = ast.literal_eval(f.read())
server = jsonrpclib.Server('http://%s:%d' % (host, port)) rpc_user, rpc_password = get_rpc_credentials(config)
if rpc_password == '':
# authentication disabled
server_url = 'http://%s:%d' % (host, port)
else:
server_url = 'http://%s:%s@%s:%d' % (
rpc_user, rpc_password, host, port)
server = jsonrpclib.Server(server_url)
# Test daemon is running # Test daemon is running
server.ping() server.ping()
return server return server
@ -87,22 +94,29 @@ def get_server(config):
time.sleep(1.0) time.sleep(1.0)
class RequestHandler(SimpleJSONRPCRequestHandler): def get_rpc_credentials(config):
rpc_user = config.get('rpcuser', None)
def do_OPTIONS(self): rpc_password = config.get('rpcpassword', None)
self.send_response(200) if rpc_user is None or rpc_password is None:
self.end_headers() rpc_user = 'user'
import ecdsa, base64
def end_headers(self): bits = 128
self.send_header("Access-Control-Allow-Headers", nbytes = bits // 8 + (bits % 8 > 0)
"Origin, X-Requested-With, Content-Type, Accept") pw_int = ecdsa.util.randrange(pow(2, bits))
self.send_header("Access-Control-Allow-Origin", "*") pw_b64 = base64.b64encode(
SimpleJSONRPCRequestHandler.end_headers(self) pw_int.to_bytes(nbytes, 'big'), b'-_')
rpc_password = to_string(pw_b64, 'ascii')
config.set_key('rpcuser', rpc_user)
config.set_key('rpcpassword', rpc_password, save=True)
elif rpc_password == '':
from .util import print_stderr
print_stderr('WARNING: RPC authentication is disabled.')
return rpc_user, rpc_password
class Daemon(DaemonThread): class Daemon(DaemonThread):
def __init__(self, config, fd): def __init__(self, config, fd, is_gui):
DaemonThread.__init__(self) DaemonThread.__init__(self)
self.config = config self.config = config
if config.get('offline'): if config.get('offline'):
@ -117,14 +131,16 @@ class Daemon(DaemonThread):
self.gui = None self.gui = None
self.wallets = {} self.wallets = {}
# Setup JSONRPC server # Setup JSONRPC server
self.cmd_runner = Commands(self.config, None, self.network) self.init_server(config, fd, is_gui)
self.init_server(config, fd)
def init_server(self, config, fd): def init_server(self, config, fd, is_gui):
host = config.get('rpchost', '127.0.0.1') host = config.get('rpchost', '127.0.0.1')
port = config.get('rpcport', 0) port = config.get('rpcport', 0)
rpc_user, rpc_password = get_rpc_credentials(config)
try: try:
server = SimpleJSONRPCServer((host, port), logRequests=False, requestHandler=RequestHandler) server = VerifyingJSONRPCServer((host, port), logRequests=False,
rpc_user=rpc_user, rpc_password=rpc_password)
except Exception as e: except Exception as e:
self.print_error('Warning: cannot initialize RPC server on host', host, e) self.print_error('Warning: cannot initialize RPC server on host', host, e)
self.server = None self.server = None
@ -132,14 +148,17 @@ class Daemon(DaemonThread):
return return
os.write(fd, bytes(repr((server.socket.getsockname(), time.time())), 'utf8')) os.write(fd, bytes(repr((server.socket.getsockname(), time.time())), 'utf8'))
os.close(fd) os.close(fd)
server.timeout = 0.1
for cmdname in known_commands:
server.register_function(getattr(self.cmd_runner, cmdname), cmdname)
server.register_function(self.run_cmdline, 'run_cmdline')
server.register_function(self.ping, 'ping')
server.register_function(self.run_daemon, 'daemon')
server.register_function(self.run_gui, 'gui')
self.server = server self.server = server
server.timeout = 0.1
server.register_function(self.ping, 'ping')
if is_gui:
server.register_function(self.run_gui, 'gui')
else:
server.register_function(self.run_daemon, 'daemon')
self.cmd_runner = Commands(self.config, None, self.network)
for cmdname in known_commands:
server.register_function(getattr(self.cmd_runner, cmdname), cmdname)
server.register_function(self.run_cmdline, 'run_cmdline')
def ping(self): def ping(self):
return True return True
@ -188,12 +207,13 @@ class Daemon(DaemonThread):
def run_gui(self, config_options): def run_gui(self, config_options):
config = SimpleConfig(config_options) config = SimpleConfig(config_options)
if self.gui: if self.gui:
if hasattr(self.gui, 'new_window'): #if hasattr(self.gui, 'new_window'):
path = config.get_wallet_path() # path = config.get_wallet_path()
self.gui.new_window(path, config.get('url')) # self.gui.new_window(path, config.get('url'))
response = "ok" # response = "ok"
else: #else:
response = "error: current GUI does not support multiple windows" # response = "error: current GUI does not support multiple windows"
response = "error: Electrum GUI already running"
else: else:
response = "Error: Electrum is running in daemon mode. Please stop the daemon first." response = "Error: Electrum is running in daemon mode. Please stop the daemon first."
return response return response

View File

@ -5,6 +5,7 @@ import sys
from threading import Thread from threading import Thread
import time import time
import csv import csv
import decimal
from decimal import Decimal from decimal import Decimal
from .bitcoin import COIN from .bitcoin import COIN
@ -389,7 +390,11 @@ class FxThread(ThreadJob):
def ccy_amount_str(self, amount, commas): def ccy_amount_str(self, amount, commas):
prec = CCY_PRECISIONS.get(self.ccy, 2) prec = CCY_PRECISIONS.get(self.ccy, 2)
fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec)) fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec))
return fmt_str.format(round(amount, prec)) try:
rounded_amount = round(amount, prec)
except decimal.InvalidOperation:
rounded_amount = amount
return fmt_str.format(rounded_amount)
def run(self): def run(self):
# This runs from the plugins thread which catches exceptions # This runs from the plugins thread which catches exceptions

95
lib/jsonrpc.py Normal file
View File

@ -0,0 +1,95 @@
#!/usr/bin/env python3
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2018 Thomas Voegtlin
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer, SimpleJSONRPCRequestHandler
from base64 import b64decode
import time
from . import util
class RPCAuthCredentialsInvalid(Exception):
def __str__(self):
return 'Authentication failed (bad credentials)'
class RPCAuthCredentialsMissing(Exception):
def __str__(self):
return 'Authentication failed (missing credentials)'
class RPCAuthUnsupportedType(Exception):
def __str__(self):
return 'Authentication failed (only basic auth is supported)'
# based on http://acooke.org/cute/BasicHTTPA0.html by andrew cooke
class VerifyingJSONRPCServer(SimpleJSONRPCServer):
def __init__(self, *args, rpc_user, rpc_password, **kargs):
self.rpc_user = rpc_user
self.rpc_password = rpc_password
class VerifyingRequestHandler(SimpleJSONRPCRequestHandler):
def parse_request(myself):
# first, call the original implementation which returns
# True if all OK so far
if SimpleJSONRPCRequestHandler.parse_request(myself):
try:
self.authenticate(myself.headers)
return True
except (RPCAuthCredentialsInvalid, RPCAuthCredentialsMissing,
RPCAuthUnsupportedType) as e:
myself.send_error(401, str(e))
except BaseException as e:
import traceback, sys
traceback.print_exc(file=sys.stderr)
myself.send_error(500, str(e))
return False
SimpleJSONRPCServer.__init__(
self, requestHandler=VerifyingRequestHandler, *args, **kargs)
def authenticate(self, headers):
if self.rpc_password == '':
# RPC authentication is disabled
return
auth_string = headers.get('Authorization', None)
if auth_string is None:
raise RPCAuthCredentialsMissing()
(basic, _, encoded) = auth_string.partition(' ')
if basic != 'Basic':
raise RPCAuthUnsupportedType()
encoded = util.to_bytes(encoded, 'utf8')
credentials = util.to_string(b64decode(encoded), 'utf8')
(username, _, password) = credentials.partition(':')
if not (util.constant_time_compare(username, self.rpc_user)
and util.constant_time_compare(password, self.rpc_password)):
time.sleep(0.050)
raise RPCAuthCredentialsInvalid()

View File

@ -171,7 +171,10 @@ class Mnemonic(object):
n_custom = int(math.ceil(math.log(custom_entropy, 2))) n_custom = int(math.ceil(math.log(custom_entropy, 2)))
n = max(16, num_bits - n_custom) n = max(16, num_bits - n_custom)
print_error("make_seed", prefix, "adding %d bits"%n) print_error("make_seed", prefix, "adding %d bits"%n)
my_entropy = ecdsa.util.randrange(pow(2, n)) my_entropy = 1
while my_entropy < pow(2, n - bpw):
# try again if seed would not contain enough words
my_entropy = ecdsa.util.randrange(pow(2, n))
nonce = 0 nonce = 0
while True: while True:
nonce += 1 nonce += 1

View File

@ -106,7 +106,8 @@ proxy_modes = ['socks4', 'socks5', 'http']
def serialize_proxy(p): def serialize_proxy(p):
if not isinstance(p, dict): if not isinstance(p, dict):
return None return None
return ':'.join([p.get('mode'),p.get('host'), p.get('port'), p.get('user'), p.get('password')]) return ':'.join([p.get('mode'), p.get('host'), p.get('port'),
p.get('user', ''), p.get('password', '')])
def deserialize_proxy(s): def deserialize_proxy(s):

View File

@ -165,7 +165,7 @@ def _parsePKCS8(_bytes):
def _parseSSLeay(bytes): def _parseSSLeay(bytes):
return _parseASN1PrivateKey(ASN1_Node(str(bytes))) return _parseASN1PrivateKey(ASN1_Node(bytes))
def bytesToNumber(s): def bytesToNumber(s):

View File

@ -49,7 +49,8 @@ def scan_barcode(device='', timeout=-1, display=True, threaded=False):
libzbar.zbar_symbol_set_first_symbol.restype = ctypes.POINTER(ctypes.c_int) libzbar.zbar_symbol_set_first_symbol.restype = ctypes.POINTER(ctypes.c_int)
proc = libzbar.zbar_processor_create(threaded) proc = libzbar.zbar_processor_create(threaded)
libzbar.zbar_processor_request_size(proc, 640, 480) libzbar.zbar_processor_request_size(proc, 640, 480)
libzbar.zbar_processor_init(proc, device, display) if libzbar.zbar_processor_init(proc, device.encode('utf-8'), display) != 0:
raise RuntimeError("Can not start QR scanner; initialization failed.")
libzbar.zbar_processor_set_visible(proc) libzbar.zbar_processor_set_visible(proc)
if libzbar.zbar_process_one(proc, timeout): if libzbar.zbar_process_one(proc, timeout):
symbols = libzbar.zbar_processor_get_results(proc) symbols = libzbar.zbar_processor_get_results(proc)

View File

@ -224,6 +224,10 @@ class TestTransaction(unittest.TestCase):
tx = transaction.Transaction('010000000001010d350cefa29138de18a2d63a93cffda63721b07a6ecfa80a902f9514104b55ca0000000000fdffffff012a4a824a00000000160014b869999d342a5d42d6dc7af1efc28456da40297a024730440220475bb55814a52ea1036919e4408218c693b8bf93637b9f54c821b5baa3b846e102207276ed7a79493142c11fb01808a4142bbdd525ae7bdccdf8ecb7b8e3c856b4d90121024cdeaca7a53a7e23a1edbe9260794eaa83063534b5f111ee3c67d8b0cb88f0eec8010000') tx = transaction.Transaction('010000000001010d350cefa29138de18a2d63a93cffda63721b07a6ecfa80a902f9514104b55ca0000000000fdffffff012a4a824a00000000160014b869999d342a5d42d6dc7af1efc28456da40297a024730440220475bb55814a52ea1036919e4408218c693b8bf93637b9f54c821b5baa3b846e102207276ed7a79493142c11fb01808a4142bbdd525ae7bdccdf8ecb7b8e3c856b4d90121024cdeaca7a53a7e23a1edbe9260794eaa83063534b5f111ee3c67d8b0cb88f0eec8010000')
self.assertEqual('51087ece75c697cc872d2e643d646b0f3e1f2666fa1820b7bff4343d50dd680e', tx.txid()) self.assertEqual('51087ece75c697cc872d2e643d646b0f3e1f2666fa1820b7bff4343d50dd680e', tx.txid())
def test_txid_input_p2wsh_p2sh_not_multisig(self):
tx = transaction.Transaction('0100000000010160f84fdcda039c3ca1b20038adea2d49a53db92f7c467e8def13734232bb610804000000232200202814720f16329ab81cb8867c4d447bd13255931f23e6655944c9ada1797fcf88ffffffff0ba3dcfc04000000001976a91488124a57c548c9e7b1dd687455af803bd5765dea88acc9f44900000000001976a914da55045a0ccd40a56ce861946d13eb861eb5f2d788ac49825e000000000017a914ca34d4b190e36479aa6e0023cfe0a8537c6aa8dd87680c0d00000000001976a914651102524c424b2e7c44787c4f21e4c54dffafc088acf02fa9000000000017a914ee6c596e6f7066466d778d4f9ba633a564a6e95d874d250900000000001976a9146ca7976b48c04fd23867748382ee8401b1d27c2988acf5119600000000001976a914cf47d5dcdba02fd547c600697097252d38c3214a88ace08a12000000000017a914017bef79d92d5ec08c051786bad317e5dd3befcf87e3d76201000000001976a9148ec1b88b66d142bcbdb42797a0fd402c23e0eec288ac718f6900000000001976a914e66344472a224ce6f843f2989accf435ae6a808988ac65e51300000000001976a914cad6717c13a2079066f876933834210ebbe68c3f88ac0347304402201a4907c4706104320313e182ecbb1b265b2d023a79586671386de86bb47461590220472c3db9fc99a728ebb9b555a72e3481d20b181bd059a9c1acadfb853d90c96c01210338a46f2a54112fef8803c8478bc17e5f8fc6a5ec276903a946c1fafb2e3a8b181976a914eda8660085bf607b82bd18560ca8f3a9ec49178588ac00000000')
self.assertEqual('e9933221a150f78f9f224899f8568ff6422ffcc28ca3d53d87936368ff7c4b1d', tx.txid())
class NetworkMock(object): class NetworkMock(object):

View File

@ -45,6 +45,14 @@ class SerializationError(Exception):
""" Thrown when there's a problem deserializing or serializing """ """ Thrown when there's a problem deserializing or serializing """
class UnknownTxinType(Exception):
pass
class NotRecognizedRedeemScript(Exception):
pass
class BCDataStream(object): class BCDataStream(object):
def __init__(self): def __init__(self):
self.input = None self.input = None
@ -302,10 +310,23 @@ def parse_scriptSig(d, _bytes):
if match_decoded(decoded, match): if match_decoded(decoded, match):
item = decoded[0][1] item = decoded[0][1]
if item[0] == 0: if item[0] == 0:
# segwit embedded into p2sh
# witness version 0
# segwit embedded into p2sh
d['address'] = bitcoin.hash160_to_p2sh(bitcoin.hash_160(item)) d['address'] = bitcoin.hash160_to_p2sh(bitcoin.hash_160(item))
d['type'] = 'p2wpkh-p2sh' if len(item) == 22 else 'p2wsh-p2sh' if len(item) == 22:
d['type'] = 'p2wpkh-p2sh'
elif len(item) == 34:
d['type'] = 'p2wsh-p2sh'
else:
print_error("unrecognized txin type", bh2u(item))
elif opcodes.OP_1 <= item[0] <= opcodes.OP_16:
# segwit embedded into p2sh
# witness version 1-16
pass
else: else:
# payto_pubkey # assert item[0] == 0x30
# pay-to-pubkey
d['type'] = 'p2pk' d['type'] = 'p2pk'
d['address'] = "(pubkey)" d['address'] = "(pubkey)"
d['signatures'] = [bh2u(item)] d['signatures'] = [bh2u(item)]
@ -361,7 +382,7 @@ def parse_redeemScript(s):
match_multisig = [ op_m ] + [opcodes.OP_PUSHDATA4]*n + [ op_n, opcodes.OP_CHECKMULTISIG ] match_multisig = [ op_m ] + [opcodes.OP_PUSHDATA4]*n + [ op_n, opcodes.OP_CHECKMULTISIG ]
if not match_decoded(dec2, match_multisig): if not match_decoded(dec2, match_multisig):
print_error("cannot find address in input script", bh2u(s)) print_error("cannot find address in input script", bh2u(s))
return raise NotRecognizedRedeemScript()
x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]] x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]]
pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys] pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys]
redeemScript = multisig_script(pubkeys, m) redeemScript = multisig_script(pubkeys, m)
@ -430,21 +451,40 @@ def parse_witness(vds, txin):
if n == 0xffffffff: if n == 0xffffffff:
txin['value'] = vds.read_uint64() txin['value'] = vds.read_uint64()
n = vds.read_compact_size() n = vds.read_compact_size()
# now 'n' is the number of items in the witness
w = list(bh2u(vds.read_bytes(vds.read_compact_size())) for i in range(n)) w = list(bh2u(vds.read_bytes(vds.read_compact_size())) for i in range(n))
add_w = lambda x: var_int(len(x) // 2) + x
txin['witness'] = var_int(n) + ''.join(add_w(i) for i in w)
# FIXME: witness version > 0 will probably fail here.
# For native segwit, we would need the scriptPubKey of the parent txn
# to determine witness program version, and properly parse the witness.
# In case of p2sh-segwit, we can tell based on the scriptSig in this txn.
# The code below assumes witness version 0.
# p2sh-segwit should work in that case; for native segwit we need to tell
# between p2wpkh and p2wsh; we do this based on number of witness items,
# hence (FIXME) p2wsh with n==2 (maybe n==1 ?) will probably fail.
# If v==0 and n==2, we need parent scriptPubKey to distinguish between p2wpkh and p2wsh.
if txin['type'] == 'coinbase': if txin['type'] == 'coinbase':
pass pass
elif n > 2: elif txin['type'] == 'p2wsh-p2sh' or n > 2:
try:
m, n, x_pubkeys, pubkeys, witnessScript = parse_redeemScript(bfh(w[-1]))
except NotRecognizedRedeemScript:
raise UnknownTxinType()
txin['signatures'] = parse_sig(w[1:-1]) txin['signatures'] = parse_sig(w[1:-1])
m, n, x_pubkeys, pubkeys, witnessScript = parse_redeemScript(bfh(w[-1]))
txin['num_sig'] = m txin['num_sig'] = m
txin['x_pubkeys'] = x_pubkeys txin['x_pubkeys'] = x_pubkeys
txin['pubkeys'] = pubkeys txin['pubkeys'] = pubkeys
txin['witnessScript'] = witnessScript txin['witnessScript'] = witnessScript
else: elif txin['type'] == 'p2wpkh-p2sh' or n == 2:
txin['num_sig'] = 1 txin['num_sig'] = 1
txin['x_pubkeys'] = [w[1]] txin['x_pubkeys'] = [w[1]]
txin['pubkeys'] = [safe_parse_pubkey(w[1])] txin['pubkeys'] = [safe_parse_pubkey(w[1])]
txin['signatures'] = parse_sig([w[0]]) txin['signatures'] = parse_sig([w[0]])
else:
raise UnknownTxinType()
def parse_output(vds, i): def parse_output(vds, i):
d = {} d = {}
@ -474,7 +514,12 @@ def deserialize(raw):
if is_segwit: if is_segwit:
for i in range(n_vin): for i in range(n_vin):
txin = d['inputs'][i] txin = d['inputs'][i]
parse_witness(vds, txin) try:
parse_witness(vds, txin)
except UnknownTxinType:
txin['type'] = 'unknown'
# FIXME: GUI might show 'unknown' address (e.g. for a non-multisig p2wsh)
continue
# segwit-native script # segwit-native script
if not txin.get('scriptSig'): if not txin.get('scriptSig'):
if txin['num_sig'] == 1: if txin['num_sig'] == 1:
@ -674,7 +719,9 @@ class Transaction:
witness_script = multisig_script(pubkeys, txin['num_sig']) witness_script = multisig_script(pubkeys, txin['num_sig'])
witness = var_int(n) + '00' + ''.join(add_w(x) for x in sig_list) + add_w(witness_script) witness = var_int(n) + '00' + ''.join(add_w(x) for x in sig_list) + add_w(witness_script)
else: else:
raise BaseException('wrong txin type') witness = txin.get('witness', None)
if not witness:
raise BaseException('wrong txin type:', txin['type'])
if self.is_txin_complete(txin) or estimate_size: if self.is_txin_complete(txin) or estimate_size:
value_field = '' value_field = ''
else: else:
@ -682,8 +729,13 @@ class Transaction:
return value_field + witness return value_field + witness
@classmethod @classmethod
def is_segwit_input(self, txin): def is_segwit_input(cls, txin):
return txin['type'] in ['p2wpkh', 'p2wpkh-p2sh', 'p2wsh', 'p2wsh-p2sh'] has_nonzero_witness = txin.get('witness', '00') != '00'
return cls.is_segwit_inputtype(txin['type']) or has_nonzero_witness
@classmethod
def is_segwit_inputtype(cls, txin_type):
return txin_type in ('p2wpkh', 'p2wpkh-p2sh', 'p2wsh', 'p2wsh-p2sh')
@classmethod @classmethod
def input_script(self, txin, estimate_size=False): def input_script(self, txin, estimate_size=False):

View File

@ -28,6 +28,7 @@ from decimal import Decimal
import traceback import traceback
import urllib import urllib
import threading import threading
import hmac
from .i18n import _ from .i18n import _
@ -196,6 +197,13 @@ def json_decode(x):
except: except:
return x return x
# taken from Django Source Code
def constant_time_compare(val1, val2):
"""Return True if the two strings are equal, False otherwise."""
return hmac.compare_digest(to_bytes(val1, 'utf8'), to_bytes(val2, 'utf8'))
# decorator that prints execution time # decorator that prints execution time
def profiler(func): def profiler(func):
def do_profile(func, args, kw_args): def do_profile(func, args, kw_args):

View File

@ -1,4 +1,4 @@
ELECTRUM_VERSION = '3.0.3' # version of the client package ELECTRUM_VERSION = '3.0.6' # version of the client package
PROTOCOL_VERSION = '1.1' # protocol version requested PROTOCOL_VERSION = '1.1' # protocol version requested
# The hash of the mnemonic seed must begin with this # The hash of the mnemonic seed must begin with this

View File

@ -361,7 +361,8 @@ class Abstract_Wallet(PrintError):
def add_unverified_tx(self, tx_hash, tx_height): def add_unverified_tx(self, tx_hash, tx_height):
if tx_height == 0 and tx_hash in self.verified_tx: if tx_height == 0 and tx_hash in self.verified_tx:
self.verified_tx.pop(tx_hash) self.verified_tx.pop(tx_hash)
self.verifier.merkle_roots.pop(tx_hash, None) if self.verifier:
self.verifier.merkle_roots.pop(tx_hash, None)
# tx will be verified only if height > 0 # tx will be verified only if height > 0
if tx_hash not in self.verified_tx: if tx_hash not in self.verified_tx:
@ -959,7 +960,7 @@ class Abstract_Wallet(PrintError):
# if we are on a pruning server, remove unverified transactions # if we are on a pruning server, remove unverified transactions
with self.lock: with self.lock:
vr = list(self.verified_tx.keys()) + list(self.unverified_tx.keys()) vr = list(self.verified_tx.keys()) + list(self.unverified_tx.keys())
for tx_hash in self.transactions.keys(): for tx_hash in list(self.transactions):
if tx_hash not in vr: if tx_hash not in vr:
self.print_error("removing transaction", tx_hash) self.print_error("removing transaction", tx_hash)
self.transactions.pop(tx_hash) self.transactions.pop(tx_hash)
@ -1435,9 +1436,6 @@ class Imported_Wallet(Simple_Wallet):
def is_deterministic(self): def is_deterministic(self):
return False return False
def is_used(self, address):
return False
def is_change(self, address): def is_change(self, address):
return False return False

View File

@ -84,7 +84,8 @@ class WsClientThread(util.DaemonThread):
l = self.subscriptions.get(addr, []) l = self.subscriptions.get(addr, [])
l.append((ws, amount)) l.append((ws, amount))
self.subscriptions[addr] = l self.subscriptions[addr] = l
self.network.send([('blockchain.address.subscribe', [addr])], self.response_queue.put) h = self.network.addr_to_scripthash(addr)
self.network.send([('blockchain.scripthash.subscribe', [h])], self.response_queue.put)
def run(self): def run(self):
@ -100,10 +101,13 @@ class WsClientThread(util.DaemonThread):
result = r.get('result') result = r.get('result')
if result is None: if result is None:
continue continue
if method == 'blockchain.address.subscribe': if method == 'blockchain.scripthash.subscribe':
self.network.send([('blockchain.address.get_balance', params)], self.response_queue.put) self.network.send([('blockchain.scripthash.get_balance', params)], self.response_queue.put)
elif method == 'blockchain.address.get_balance': elif method == 'blockchain.scripthash.get_balance':
addr = params[0] h = params[0]
addr = self.network.h2addr.get(h, None)
if addr is None:
util.print_error("can't find address for scripthash: %s" % h)
l = self.subscriptions.get(addr, []) l = self.subscriptions.get(addr, [])
for ws, amount in l: for ws, amount in l:
if not ws.closed: if not ws.closed:

View File

@ -25,6 +25,12 @@ try:
except ImportError: except ImportError:
BTCHIP = False BTCHIP = False
MSG_NEEDS_FW_UPDATE_GENERIC = _('Firmware version too old. Please update at') + \
' https://www.ledgerwallet.com'
MSG_NEEDS_FW_UPDATE_SEGWIT = _('Firmware version (or "Bitcoin" app) too old for Segwit support. Please update at') + \
' https://www.ledgerwallet.com'
class Ledger_Client(): class Ledger_Client():
def __init__(self, hidDevice): def __init__(self, hidDevice):
self.dongleObject = btchip(hidDevice) self.dongleObject = btchip(hidDevice)
@ -46,8 +52,23 @@ class Ledger_Client():
return "" return ""
def i4b(self, x): def i4b(self, x):
return pack('>I', x) return pack('>I', x)
def test_pin_unlocked(func):
"""Function decorator to test the Ledger for being unlocked, and if not,
raise a human-readable exception.
"""
def catch_exception(self, *args, **kwargs):
try:
return func(self, *args, **kwargs)
except BTChipException as e:
if e.sw == 0x6982:
raise Exception(_('Your Ledger is locked. Please unlock it.'))
else:
raise
return catch_exception
@test_pin_unlocked
def get_xpub(self, bip32_path, xtype): def get_xpub(self, bip32_path, xtype):
self.checkDevice() self.checkDevice()
# bip32_path is of the form 44'/0'/1' # bip32_path is of the form 44'/0'/1'
@ -57,9 +78,9 @@ class Ledger_Client():
#self.get_client() # prompt for the PIN before displaying the dialog if necessary #self.get_client() # prompt for the PIN before displaying the dialog if necessary
#self.handler.show_message("Computing master public key") #self.handler.show_message("Computing master public key")
if xtype in ['p2wpkh', 'p2wsh'] and not self.supports_native_segwit(): if xtype in ['p2wpkh', 'p2wsh'] and not self.supports_native_segwit():
raise Exception("Firmware version too old for Segwit support. Please update at https://www.ledgerwallet.com") raise Exception(MSG_NEEDS_FW_UPDATE_SEGWIT)
if xtype in ['p2wpkh-p2sh', 'p2wsh-p2sh'] and not self.supports_segwit(): if xtype in ['p2wpkh-p2sh', 'p2wsh-p2sh'] and not self.supports_segwit():
raise Exception("Firmware version too old for Segwit support. Please update at https://www.ledgerwallet.com") raise Exception(MSG_NEEDS_FW_UPDATE_SEGWIT)
splitPath = bip32_path.split('/') splitPath = bip32_path.split('/')
if splitPath[0] == 'm': if splitPath[0] == 'm':
splitPath = splitPath[1:] splitPath = splitPath[1:]
@ -68,7 +89,7 @@ class Ledger_Client():
if len(splitPath) > 1: if len(splitPath) > 1:
prevPath = "/".join(splitPath[0:len(splitPath) - 1]) prevPath = "/".join(splitPath[0:len(splitPath) - 1])
nodeData = self.dongleObject.getWalletPublicKey(prevPath) nodeData = self.dongleObject.getWalletPublicKey(prevPath)
publicKey = compress_public_key(nodeData['publicKey'])# publicKey = compress_public_key(nodeData['publicKey'])
h = hashlib.new('ripemd160') h = hashlib.new('ripemd160')
h.update(hashlib.sha256(publicKey).digest()) h.update(hashlib.sha256(publicKey).digest())
fingerprint = unpack(">I", h.digest()[0:4])[0] fingerprint = unpack(">I", h.digest()[0:4])[0]
@ -119,7 +140,7 @@ class Ledger_Client():
if not checkFirmware(firmware): if not checkFirmware(firmware):
self.dongleObject.dongle.close() self.dongleObject.dongle.close()
raise Exception("HW1 firmware version too old. Please update at https://www.ledgerwallet.com") raise Exception(MSG_NEEDS_FW_UPDATE_GENERIC)
try: try:
self.dongleObject.getOperationMode() self.dongleObject.getOperationMode()
except BTChipException as e: except BTChipException as e:
@ -183,14 +204,14 @@ class Ledger_KeyStore(Hardware_KeyStore):
return obj return obj
def get_derivation(self): def get_derivation(self):
return self.derivation return self.derivation
def get_client(self): def get_client(self):
return self.plugin.get_client(self).dongleObject return self.plugin.get_client(self).dongleObject
def get_client_electrum(self): def get_client_electrum(self):
return self.plugin.get_client(self) return self.plugin.get_client(self)
def give_error(self, message, clear_client = False): def give_error(self, message, clear_client = False):
print_error(message) print_error(message)
if not self.signing: if not self.signing:
@ -285,12 +306,12 @@ class Ledger_KeyStore(Hardware_KeyStore):
if txin['type'] in ['p2wpkh-p2sh', 'p2wsh-p2sh']: if txin['type'] in ['p2wpkh-p2sh', 'p2wsh-p2sh']:
if not self.get_client_electrum().supports_segwit(): if not self.get_client_electrum().supports_segwit():
self.give_error("Firmware version too old to support segwit. Please update at https://www.ledgerwallet.com") self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT)
segwitTransaction = True segwitTransaction = True
if txin['type'] in ['p2wpkh', 'p2wsh']: if txin['type'] in ['p2wpkh', 'p2wsh']:
if not self.get_client_electrum().supports_native_segwit(): if not self.get_client_electrum().supports_native_segwit():
self.give_error("Firmware version too old to support native segwit. Please update at https://www.ledgerwallet.com") self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT)
segwitTransaction = True segwitTransaction = True
pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin)
@ -342,10 +363,10 @@ class Ledger_KeyStore(Hardware_KeyStore):
self.handler.show_message(_("Confirm Transaction on your Ledger device...")) self.handler.show_message(_("Confirm Transaction on your Ledger device..."))
try: try:
# Get trusted inputs from the original transactions # Get trusted inputs from the original transactions
for utxo in inputs: for utxo in inputs:
sequence = int_to_hex(utxo[5], 4) sequence = int_to_hex(utxo[5], 4)
if segwitTransaction: if segwitTransaction:
txtmp = bitcoinTransaction(bfh(utxo[0])) txtmp = bitcoinTransaction(bfh(utxo[0]))
tmp = bfh(utxo[3])[::-1] tmp = bfh(utxo[3])[::-1]
tmp += bfh(int_to_hex(utxo[1], 4)) tmp += bfh(int_to_hex(utxo[1], 4))
tmp += txtmp.outputs[utxo[1]].amount tmp += txtmp.outputs[utxo[1]].amount
@ -434,7 +455,7 @@ class LedgerPlugin(HW_PluginBase):
libraries_available = BTCHIP libraries_available = BTCHIP
keystore_class = Ledger_KeyStore keystore_class = Ledger_KeyStore
client = None client = None
DEVICE_IDS = [ DEVICE_IDS = [
(0x2581, 0x1807), # HW.1 legacy btchip (0x2581, 0x1807), # HW.1 legacy btchip
(0x2581, 0x2b7c), # HW.1 transitional production (0x2581, 0x2b7c), # HW.1 transitional production
(0x2581, 0x3b7c), # HW.1 ledger production (0x2581, 0x3b7c), # HW.1 ledger production
@ -459,12 +480,12 @@ class LedgerPlugin(HW_PluginBase):
def get_btchip_device(self, device): def get_btchip_device(self, device):
ledger = False ledger = False
if (device.product_key[0] == 0x2581 and device.product_key[1] == 0x3b7c) or (device.product_key[0] == 0x2581 and device.product_key[1] == 0x4b7c) or (device.product_key[0] == 0x2c97): if (device.product_key[0] == 0x2581 and device.product_key[1] == 0x3b7c) or (device.product_key[0] == 0x2581 and device.product_key[1] == 0x4b7c) or (device.product_key[0] == 0x2c97):
ledger = True ledger = True
dev = hid.device() dev = hid.device()
dev.open_path(device.path) dev.open_path(device.path)
dev.set_nonblocking(True) dev.set_nonblocking(True)
return HIDDongleHIDAPI(dev, ledger, BTCHIP_DEBUG) return HIDDongleHIDAPI(dev, ledger, BTCHIP_DEBUG)
def create_client(self, device, handler): def create_client(self, device, handler):
self.handler = handler self.handler = handler
@ -473,7 +494,7 @@ class LedgerPlugin(HW_PluginBase):
client = Ledger_Client(client) client = Ledger_Client(client)
return client return client
def setup_device(self, device_info, wizard): def setup_device(self, device_info, wizard):
devmgr = self.device_manager() devmgr = self.device_manager()
device_id = device_info.device.id_ device_id = device_info.device.id_
client = devmgr.client_by_id(device_id) client = devmgr.client_by_id(device_id)
@ -494,10 +515,10 @@ class LedgerPlugin(HW_PluginBase):
devmgr = self.device_manager() devmgr = self.device_manager()
handler = keystore.handler handler = keystore.handler
with devmgr.hid_lock: with devmgr.hid_lock:
client = devmgr.client_for_keystore(self, handler, keystore, force_pair) client = devmgr.client_for_keystore(self, handler, keystore, force_pair)
# returns the client for a given keystore. can use xpub # returns the client for a given keystore. can use xpub
#if client: #if client:
# client.used() # client.used()
if client is not None: if client is not None:
client.checkDevice() client.checkDevice()
return client return client