Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82e88cb89d | ||
|
|
d76f36df70 | ||
|
|
cf82b1d9d5 | ||
|
|
29206852fb | ||
|
|
f0942a2535 | ||
|
|
7b8b75c7d3 | ||
|
|
b0966671d7 | ||
|
|
64fa7dd6c3 | ||
|
|
d0433c1539 | ||
|
|
ff38e90405 | ||
|
|
c79662fbce | ||
|
|
d5e20d607e | ||
|
|
53fc343b4a | ||
|
|
70d6f50d94 | ||
|
|
1c4773d41a | ||
|
|
7cfa3c2d51 | ||
|
|
47f57af145 | ||
|
|
7279fc8902 | ||
|
|
87737dbe45 | ||
|
|
231f4931d6 | ||
|
|
a9973ce6ab | ||
|
|
1fc45132c0 | ||
|
|
e65353c062 | ||
|
|
0045784a58 | ||
|
|
79d402d3f9 | ||
|
|
063ec0a758 | ||
|
|
d4f1445914 | ||
|
|
498a269c88 | ||
|
|
b7c20e71ac | ||
|
|
a6e59499db | ||
|
|
d3a963e673 | ||
|
|
e4308a360b | ||
|
|
e98406fc7c | ||
|
|
dfaf4817c9 | ||
|
|
0f54051ecb | ||
|
|
02fda5a85b | ||
|
|
d9925967b7 | ||
|
|
006aece3a3 | ||
|
|
c7a47a06b5 | ||
|
|
fdd10bfb60 |
@ -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
|
||||
* 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
|
||||
|
||||
@ -43,6 +43,7 @@ done
|
||||
popd
|
||||
|
||||
pushd electrum
|
||||
git checkout $BRANCH
|
||||
VERSION=`git describe --tags`
|
||||
echo "Last commit: $VERSION"
|
||||
find -exec touch -d '2000-11-11T11:11:11+00:00' {} +
|
||||
|
||||
@ -23,6 +23,6 @@ cd tmp
|
||||
$PYTHON -m pip install setuptools --upgrade
|
||||
$PYTHON -m pip install cython --upgrade
|
||||
$PYTHON -m pip install trezor==0.7.16 --upgrade
|
||||
$PYTHON -m pip install keepkey==4.0.0 --upgrade
|
||||
$PYTHON -m pip install btchip-python==0.1.23 --upgrade
|
||||
$PYTHON -m pip install keepkey==4.0.2 --upgrade
|
||||
$PYTHON -m pip install btchip-python==0.1.24 --upgrade
|
||||
|
||||
|
||||
8
electrum
8
electrum
@ -278,7 +278,8 @@ def run_offline_command(config, config_options):
|
||||
# arguments passed to function
|
||||
args = [config.get(x) for x in cmd.params]
|
||||
# decode json arguments
|
||||
args = list(map(json_decode, args))
|
||||
if cmdname not in ('setconfig',):
|
||||
args = list(map(json_decode, args))
|
||||
# options
|
||||
kwargs = {}
|
||||
for x in cmd.options:
|
||||
@ -372,7 +373,7 @@ if __name__ == '__main__':
|
||||
fd, server = daemon.get_fd_or_server(config)
|
||||
if fd is not None:
|
||||
plugins = init_plugins(config, config.get('gui', 'qt'))
|
||||
d = daemon.Daemon(config, fd)
|
||||
d = daemon.Daemon(config, fd, True)
|
||||
d.start()
|
||||
d.init_gui(config, plugins)
|
||||
sys.exit(0)
|
||||
@ -393,7 +394,7 @@ if __name__ == '__main__':
|
||||
print_stderr("starting daemon (PID %d)" % pid)
|
||||
sys.exit(0)
|
||||
init_plugins(config, 'cmdline')
|
||||
d = daemon.Daemon(config, fd)
|
||||
d = daemon.Daemon(config, fd, False)
|
||||
d.start()
|
||||
if config.get('websocket_server'):
|
||||
from electrum import websockets
|
||||
@ -425,7 +426,6 @@ if __name__ == '__main__':
|
||||
else:
|
||||
init_plugins(config, 'cmdline')
|
||||
result = run_offline_command(config, config_options)
|
||||
|
||||
# print result
|
||||
if isinstance(result, str):
|
||||
print_msg(result)
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -325,7 +325,7 @@ class ElectrumWindow(App):
|
||||
|
||||
@profiler
|
||||
def update_tabs(self):
|
||||
for tab in ['invoices', 'send', 'history', 'receive', 'requests']:
|
||||
for tab in ['invoices', 'send', 'history', 'receive', 'address']:
|
||||
self.update_tab(tab)
|
||||
|
||||
def switch_to(self, name):
|
||||
@ -384,45 +384,22 @@ class ElectrumWindow(App):
|
||||
def scan_qr(self, on_complete):
|
||||
if platform != 'android':
|
||||
return
|
||||
from jnius import autoclass
|
||||
from jnius import autoclass, cast
|
||||
from android import activity
|
||||
PythonActivity = autoclass('org.kivy.android.PythonActivity')
|
||||
SimpleScannerActivity = autoclass("org.electrum.qr.SimpleScannerActivity")
|
||||
Intent = autoclass('android.content.Intent')
|
||||
intent = Intent("com.google.zxing.client.android.SCAN")
|
||||
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'))
|
||||
intent = Intent(PythonActivity.mActivity, SimpleScannerActivity)
|
||||
|
||||
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):
|
||||
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"))
|
||||
if resultCode == -1: # RESULT_OK:
|
||||
# this doesn't work due to some bug in jnius:
|
||||
# contents = intent.getStringExtra("text")
|
||||
String = autoclass("java.lang.String")
|
||||
contents = intent.getStringExtra(String("text"))
|
||||
on_complete(contents)
|
||||
activity.bind(on_activity_result=on_qr_result)
|
||||
integrator.initiateScan()
|
||||
PythonActivity.mActivity.startActivityForResult(intent, 0)
|
||||
|
||||
def do_share(self, data, title):
|
||||
if platform != 'android':
|
||||
@ -943,9 +920,18 @@ class ElectrumWindow(App):
|
||||
self._password_dialog.open()
|
||||
|
||||
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):
|
||||
if self.wallet.has_password() and password is None:
|
||||
return
|
||||
key = str(self.wallet.export_private_key(addr, password)[0])
|
||||
pk_label.data = key
|
||||
if not self.wallet.can_export():
|
||||
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))
|
||||
|
||||
@ -52,7 +52,8 @@ fullscreen = False
|
||||
#
|
||||
|
||||
# (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
|
||||
#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
|
||||
# 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
|
||||
# not yet merged features.
|
||||
|
||||
@ -521,7 +521,7 @@ class AddressScreen(CScreen):
|
||||
def update(self):
|
||||
self.menu_actions = [('Receive', self.do_show), ('Details', self.do_view)]
|
||||
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
|
||||
container = self.screen.ids.search_container
|
||||
container.clear_widgets()
|
||||
|
||||
@ -240,6 +240,7 @@ class ElectrumGui:
|
||||
except GoBack:
|
||||
return
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
return
|
||||
self.timer.start()
|
||||
@ -248,11 +249,23 @@ class ElectrumGui:
|
||||
if not self.start_new_window(path, self.config.get('url')):
|
||||
return
|
||||
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
|
||||
self.app.exec_()
|
||||
# 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()
|
||||
# on some platforms the exec_ call may not return, so use clean_up()
|
||||
|
||||
@ -93,6 +93,6 @@ class AddressDialog(WindowModalDialog):
|
||||
def show_qr(self):
|
||||
text = self.address
|
||||
try:
|
||||
self.parent.show_qrcode(text, 'Address')
|
||||
self.parent.show_qrcode(text, 'Address', parent=self)
|
||||
except Exception as e:
|
||||
self.show_message(str(e))
|
||||
|
||||
@ -527,10 +527,10 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
||||
grid.addWidget(m_label, 1, 0)
|
||||
grid.addWidget(m_edit, 1, 1)
|
||||
def on_m(m):
|
||||
m_label.setText(_('Require %d signatures')%m)
|
||||
m_label.setText(_('Require {0} signatures').format(m))
|
||||
cw.set_m(m)
|
||||
def on_n(n):
|
||||
n_label.setText(_('From %d cosigners')%n)
|
||||
n_label.setText(_('From {0} cosigners').format(n))
|
||||
cw.set_n(n)
|
||||
m_edit.setMaximum(n)
|
||||
n_edit.valueChanged.connect(on_n)
|
||||
|
||||
@ -2164,6 +2164,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||
self.show_message(_("This is a watching-only wallet"))
|
||||
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.setMinimumSize(850, 300)
|
||||
vbox = QVBoxLayout(d)
|
||||
@ -2189,25 +2193,38 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||
private_keys = {}
|
||||
addresses = self.wallet.get_addresses()
|
||||
done = False
|
||||
cancelled = False
|
||||
def privkeys_thread():
|
||||
for addr in addresses:
|
||||
time.sleep(0.1)
|
||||
if done:
|
||||
if done or cancelled:
|
||||
break
|
||||
privkey = self.wallet.export_private_key(addr, password)[0]
|
||||
private_keys[addr] = privkey
|
||||
self.computing_privkeys_signal.emit()
|
||||
self.computing_privkeys_signal.disconnect()
|
||||
self.show_privkeys_signal.emit()
|
||||
if not cancelled:
|
||||
self.computing_privkeys_signal.disconnect()
|
||||
self.show_privkeys_signal.emit()
|
||||
|
||||
def show_privkeys():
|
||||
s = "\n".join( map( lambda x: x[0] + "\t"+ x[1], private_keys.items()))
|
||||
e.setText(s)
|
||||
b.setEnabled(True)
|
||||
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.show_privkeys_signal.connect(show_privkeys)
|
||||
d.finished.connect(on_dialog_closed)
|
||||
threading.Thread(target=privkeys_thread).start()
|
||||
|
||||
if not d.exec_():
|
||||
@ -2437,6 +2454,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||
self.fiat_receive_e.setVisible(b)
|
||||
self.history_list.refresh_headers()
|
||||
self.history_list.update()
|
||||
self.address_list.refresh_headers()
|
||||
self.address_list.update()
|
||||
self.update_status()
|
||||
|
||||
@ -2587,7 +2605,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||
unit_combo = QComboBox()
|
||||
unit_combo.addItems(units)
|
||||
unit_combo.setCurrentIndex(units.index(self.base_unit()))
|
||||
def on_unit(x):
|
||||
def on_unit(x, nz):
|
||||
unit_result = units[unit_combo.currentIndex()]
|
||||
if self.base_unit() == unit_result:
|
||||
return
|
||||
@ -2602,13 +2620,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||
else:
|
||||
raise Exception('Unknown base unit')
|
||||
self.config.set_key('decimal_point', self.decimal_point, True)
|
||||
nz.setMaximum(self.decimal_point)
|
||||
self.history_list.update()
|
||||
self.request_list.update()
|
||||
self.address_list.update()
|
||||
for edit, amount in zip(edits, amounts):
|
||||
edit.setAmount(amount)
|
||||
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))
|
||||
|
||||
block_explorers = sorted(util.block_explorer_info().keys())
|
||||
|
||||
@ -355,14 +355,14 @@ class NetworkChoiceLayout(object):
|
||||
height_str = "%d "%(self.network.get_local_height()) + _('blocks')
|
||||
self.height_label.setText(height_str)
|
||||
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)
|
||||
chains = self.network.get_blockchains()
|
||||
if len(chains)>1:
|
||||
chain = self.network.blockchain()
|
||||
checkpoint = chain.get_checkpoint()
|
||||
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 += ' (%d %s)' % (chain.get_branch_size(), _('blocks'))
|
||||
else:
|
||||
|
||||
@ -113,8 +113,7 @@ class QRDialog(WindowModalDialog):
|
||||
|
||||
def copy_to_clipboard():
|
||||
p = qscreen.grabWindow(qrw.winId())
|
||||
p.save(filename, 'png')
|
||||
QApplication.clipboard().setImage(QImage(filename))
|
||||
QApplication.clipboard().setPixmap(p)
|
||||
self.show_message(_("QR code copied to clipboard"))
|
||||
|
||||
b = QPushButton(_("Copy"))
|
||||
|
||||
@ -56,6 +56,8 @@ class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin):
|
||||
except BaseException as e:
|
||||
self.show_error(str(e))
|
||||
data = ''
|
||||
if not data:
|
||||
data = ''
|
||||
self.setText(data)
|
||||
return data
|
||||
|
||||
|
||||
@ -35,7 +35,7 @@ from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit
|
||||
def seed_warning_msg(seed):
|
||||
return ''.join([
|
||||
"<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 "
|
||||
"of computer failure."),
|
||||
"</p>",
|
||||
@ -45,7 +45,7 @@ def seed_warning_msg(seed):
|
||||
"<li>" + _("Never type it on a website.") + "</li>",
|
||||
"<li>" + _("Do not store it electronically.") + "</li>",
|
||||
"</ul>"
|
||||
]) % len(seed.split())
|
||||
]).format(len(seed.split()))
|
||||
|
||||
|
||||
class SeedLayout(QVBoxLayout):
|
||||
|
||||
16
gui/stdio.py
16
gui/stdio.py
@ -85,12 +85,11 @@ class ElectrumGui:
|
||||
delta = (80 - sum(width) - 4)/3
|
||||
format_str = "%"+"%d"%width[0]+"s"+"%"+"%d"%(width[1]+delta)+"s"+"%" \
|
||||
+ "%d"%(width[2]+delta)+"s"+"%"+"%d"%(width[3]+delta)+"s"
|
||||
b = 0
|
||||
messages = []
|
||||
|
||||
for item in self.wallet.get_history():
|
||||
tx_hash, confirmations, value, timestamp, balance = item
|
||||
if confirmations:
|
||||
tx_hash, height, conf, timestamp, delta, balance = item
|
||||
if conf:
|
||||
try:
|
||||
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
|
||||
except Exception:
|
||||
@ -99,7 +98,7 @@ class ElectrumGui:
|
||||
time_str = 'unconfirmed'
|
||||
|
||||
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")))
|
||||
|
||||
@ -149,12 +148,13 @@ class ElectrumGui:
|
||||
for i, x in enumerate( self.wallet.network.banner.split('\n') ):
|
||||
print( x )
|
||||
|
||||
def print_list(self, list, firstline):
|
||||
self.maxpos = len(list)
|
||||
def print_list(self, lst, firstline):
|
||||
lst = list(lst)
|
||||
self.maxpos = len(lst)
|
||||
if not self.maxpos: return
|
||||
print(firstline)
|
||||
for i in range(self.maxpos):
|
||||
msg = list[i] if i < len(list) else ""
|
||||
msg = lst[i] if i < len(lst) else ""
|
||||
print(msg)
|
||||
|
||||
|
||||
@ -176,7 +176,7 @@ class ElectrumGui:
|
||||
print(_('Invalid Fee'))
|
||||
return
|
||||
|
||||
if self.wallet.use_encryption:
|
||||
if self.wallet.has_password():
|
||||
password = self.password_dialog()
|
||||
if not password:
|
||||
return
|
||||
|
||||
BIN
icons/unpaid.png
BIN
icons/unpaid.png
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 23 KiB |
@ -34,7 +34,7 @@ from functools import wraps
|
||||
from decimal import Decimal
|
||||
|
||||
from .import util
|
||||
from .util import bfh, bh2u, format_satoshis
|
||||
from .util import bfh, bh2u, format_satoshis, json_decode
|
||||
from .import bitcoin
|
||||
from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS
|
||||
from .i18n import _
|
||||
@ -151,10 +151,8 @@ class Commands:
|
||||
@command('')
|
||||
def setconfig(self, key, value):
|
||||
"""Set a configuration variable. 'value' may be a string or a Python expression."""
|
||||
try:
|
||||
value = ast.literal_eval(value)
|
||||
except:
|
||||
pass
|
||||
if key not in ('rpcuser', 'rpcpassword'):
|
||||
value = json_decode(value)
|
||||
self.config.set_key(key, value)
|
||||
return True
|
||||
|
||||
|
||||
@ -28,12 +28,12 @@ import time
|
||||
|
||||
# from jsonrpc import JSONRPCResponseManager
|
||||
import jsonrpclib
|
||||
from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer, SimpleJSONRPCRequestHandler
|
||||
from .jsonrpc import VerifyingJSONRPCServer
|
||||
|
||||
from .version import ELECTRUM_VERSION
|
||||
from .network import Network
|
||||
from .util import json_decode, DaemonThread
|
||||
from .util import print_error
|
||||
from .util import print_error, to_string
|
||||
from .wallet import Wallet
|
||||
from .storage import WalletStorage
|
||||
from .commands import known_commands, Commands
|
||||
@ -75,7 +75,14 @@ def get_server(config):
|
||||
try:
|
||||
with open(lockfile) as f:
|
||||
(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
|
||||
server.ping()
|
||||
return server
|
||||
@ -87,22 +94,29 @@ def get_server(config):
|
||||
time.sleep(1.0)
|
||||
|
||||
|
||||
class RequestHandler(SimpleJSONRPCRequestHandler):
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
|
||||
def end_headers(self):
|
||||
self.send_header("Access-Control-Allow-Headers",
|
||||
"Origin, X-Requested-With, Content-Type, Accept")
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
SimpleJSONRPCRequestHandler.end_headers(self)
|
||||
def get_rpc_credentials(config):
|
||||
rpc_user = config.get('rpcuser', None)
|
||||
rpc_password = config.get('rpcpassword', None)
|
||||
if rpc_user is None or rpc_password is None:
|
||||
rpc_user = 'user'
|
||||
import ecdsa, base64
|
||||
bits = 128
|
||||
nbytes = bits // 8 + (bits % 8 > 0)
|
||||
pw_int = ecdsa.util.randrange(pow(2, bits))
|
||||
pw_b64 = base64.b64encode(
|
||||
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):
|
||||
|
||||
def __init__(self, config, fd):
|
||||
def __init__(self, config, fd, is_gui):
|
||||
DaemonThread.__init__(self)
|
||||
self.config = config
|
||||
if config.get('offline'):
|
||||
@ -117,14 +131,16 @@ class Daemon(DaemonThread):
|
||||
self.gui = None
|
||||
self.wallets = {}
|
||||
# Setup JSONRPC server
|
||||
self.cmd_runner = Commands(self.config, None, self.network)
|
||||
self.init_server(config, fd)
|
||||
self.init_server(config, fd, is_gui)
|
||||
|
||||
def init_server(self, config, fd):
|
||||
def init_server(self, config, fd, is_gui):
|
||||
host = config.get('rpchost', '127.0.0.1')
|
||||
port = config.get('rpcport', 0)
|
||||
|
||||
rpc_user, rpc_password = get_rpc_credentials(config)
|
||||
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:
|
||||
self.print_error('Warning: cannot initialize RPC server on host', host, e)
|
||||
self.server = None
|
||||
@ -132,14 +148,17 @@ class Daemon(DaemonThread):
|
||||
return
|
||||
os.write(fd, bytes(repr((server.socket.getsockname(), time.time())), 'utf8'))
|
||||
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
|
||||
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):
|
||||
return True
|
||||
@ -188,12 +207,13 @@ class Daemon(DaemonThread):
|
||||
def run_gui(self, config_options):
|
||||
config = SimpleConfig(config_options)
|
||||
if self.gui:
|
||||
if hasattr(self.gui, 'new_window'):
|
||||
path = config.get_wallet_path()
|
||||
self.gui.new_window(path, config.get('url'))
|
||||
response = "ok"
|
||||
else:
|
||||
response = "error: current GUI does not support multiple windows"
|
||||
#if hasattr(self.gui, 'new_window'):
|
||||
# path = config.get_wallet_path()
|
||||
# self.gui.new_window(path, config.get('url'))
|
||||
# response = "ok"
|
||||
#else:
|
||||
# response = "error: current GUI does not support multiple windows"
|
||||
response = "error: Electrum GUI already running"
|
||||
else:
|
||||
response = "Error: Electrum is running in daemon mode. Please stop the daemon first."
|
||||
return response
|
||||
|
||||
@ -5,6 +5,7 @@ import sys
|
||||
from threading import Thread
|
||||
import time
|
||||
import csv
|
||||
import decimal
|
||||
from decimal import Decimal
|
||||
|
||||
from .bitcoin import COIN
|
||||
@ -389,7 +390,11 @@ class FxThread(ThreadJob):
|
||||
def ccy_amount_str(self, amount, commas):
|
||||
prec = CCY_PRECISIONS.get(self.ccy, 2)
|
||||
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):
|
||||
# This runs from the plugins thread which catches exceptions
|
||||
|
||||
95
lib/jsonrpc.py
Normal file
95
lib/jsonrpc.py
Normal 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()
|
||||
@ -171,7 +171,10 @@ class Mnemonic(object):
|
||||
n_custom = int(math.ceil(math.log(custom_entropy, 2)))
|
||||
n = max(16, num_bits - n_custom)
|
||||
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
|
||||
while True:
|
||||
nonce += 1
|
||||
|
||||
@ -106,7 +106,8 @@ proxy_modes = ['socks4', 'socks5', 'http']
|
||||
def serialize_proxy(p):
|
||||
if not isinstance(p, dict):
|
||||
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):
|
||||
|
||||
@ -165,7 +165,7 @@ def _parsePKCS8(_bytes):
|
||||
|
||||
|
||||
def _parseSSLeay(bytes):
|
||||
return _parseASN1PrivateKey(ASN1_Node(str(bytes)))
|
||||
return _parseASN1PrivateKey(ASN1_Node(bytes))
|
||||
|
||||
|
||||
def bytesToNumber(s):
|
||||
|
||||
@ -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)
|
||||
proc = libzbar.zbar_processor_create(threaded)
|
||||
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)
|
||||
if libzbar.zbar_process_one(proc, timeout):
|
||||
symbols = libzbar.zbar_processor_get_results(proc)
|
||||
|
||||
@ -224,6 +224,10 @@ class TestTransaction(unittest.TestCase):
|
||||
tx = transaction.Transaction('010000000001010d350cefa29138de18a2d63a93cffda63721b07a6ecfa80a902f9514104b55ca0000000000fdffffff012a4a824a00000000160014b869999d342a5d42d6dc7af1efc28456da40297a024730440220475bb55814a52ea1036919e4408218c693b8bf93637b9f54c821b5baa3b846e102207276ed7a79493142c11fb01808a4142bbdd525ae7bdccdf8ecb7b8e3c856b4d90121024cdeaca7a53a7e23a1edbe9260794eaa83063534b5f111ee3c67d8b0cb88f0eec8010000')
|
||||
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):
|
||||
|
||||
|
||||
@ -45,6 +45,14 @@ class SerializationError(Exception):
|
||||
""" Thrown when there's a problem deserializing or serializing """
|
||||
|
||||
|
||||
class UnknownTxinType(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotRecognizedRedeemScript(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BCDataStream(object):
|
||||
def __init__(self):
|
||||
self.input = None
|
||||
@ -302,10 +310,23 @@ def parse_scriptSig(d, _bytes):
|
||||
if match_decoded(decoded, match):
|
||||
item = decoded[0][1]
|
||||
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['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:
|
||||
# payto_pubkey
|
||||
# assert item[0] == 0x30
|
||||
# pay-to-pubkey
|
||||
d['type'] = 'p2pk'
|
||||
d['address'] = "(pubkey)"
|
||||
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 ]
|
||||
if not match_decoded(dec2, match_multisig):
|
||||
print_error("cannot find address in input script", bh2u(s))
|
||||
return
|
||||
raise NotRecognizedRedeemScript()
|
||||
x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]]
|
||||
pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys]
|
||||
redeemScript = multisig_script(pubkeys, m)
|
||||
@ -430,21 +451,40 @@ def parse_witness(vds, txin):
|
||||
if n == 0xffffffff:
|
||||
txin['value'] = vds.read_uint64()
|
||||
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))
|
||||
|
||||
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':
|
||||
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])
|
||||
m, n, x_pubkeys, pubkeys, witnessScript = parse_redeemScript(bfh(w[-1]))
|
||||
txin['num_sig'] = m
|
||||
txin['x_pubkeys'] = x_pubkeys
|
||||
txin['pubkeys'] = pubkeys
|
||||
txin['witnessScript'] = witnessScript
|
||||
else:
|
||||
elif txin['type'] == 'p2wpkh-p2sh' or n == 2:
|
||||
txin['num_sig'] = 1
|
||||
txin['x_pubkeys'] = [w[1]]
|
||||
txin['pubkeys'] = [safe_parse_pubkey(w[1])]
|
||||
txin['signatures'] = parse_sig([w[0]])
|
||||
else:
|
||||
raise UnknownTxinType()
|
||||
|
||||
def parse_output(vds, i):
|
||||
d = {}
|
||||
@ -474,7 +514,12 @@ def deserialize(raw):
|
||||
if is_segwit:
|
||||
for i in range(n_vin):
|
||||
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
|
||||
if not txin.get('scriptSig'):
|
||||
if txin['num_sig'] == 1:
|
||||
@ -674,7 +719,9 @@ class Transaction:
|
||||
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)
|
||||
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:
|
||||
value_field = ''
|
||||
else:
|
||||
@ -682,8 +729,13 @@ class Transaction:
|
||||
return value_field + witness
|
||||
|
||||
@classmethod
|
||||
def is_segwit_input(self, txin):
|
||||
return txin['type'] in ['p2wpkh', 'p2wpkh-p2sh', 'p2wsh', 'p2wsh-p2sh']
|
||||
def is_segwit_input(cls, txin):
|
||||
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
|
||||
def input_script(self, txin, estimate_size=False):
|
||||
|
||||
@ -28,6 +28,7 @@ from decimal import Decimal
|
||||
import traceback
|
||||
import urllib
|
||||
import threading
|
||||
import hmac
|
||||
|
||||
from .i18n import _
|
||||
|
||||
@ -196,6 +197,13 @@ def json_decode(x):
|
||||
except:
|
||||
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
|
||||
def profiler(func):
|
||||
def do_profile(func, args, kw_args):
|
||||
|
||||
@ -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
|
||||
|
||||
# The hash of the mnemonic seed must begin with this
|
||||
|
||||
@ -361,7 +361,8 @@ class Abstract_Wallet(PrintError):
|
||||
def add_unverified_tx(self, tx_hash, tx_height):
|
||||
if tx_height == 0 and tx_hash in self.verified_tx:
|
||||
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
|
||||
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
|
||||
with self.lock:
|
||||
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:
|
||||
self.print_error("removing transaction", tx_hash)
|
||||
self.transactions.pop(tx_hash)
|
||||
@ -1435,9 +1436,6 @@ class Imported_Wallet(Simple_Wallet):
|
||||
def is_deterministic(self):
|
||||
return False
|
||||
|
||||
def is_used(self, address):
|
||||
return False
|
||||
|
||||
def is_change(self, address):
|
||||
return False
|
||||
|
||||
|
||||
@ -84,7 +84,8 @@ class WsClientThread(util.DaemonThread):
|
||||
l = self.subscriptions.get(addr, [])
|
||||
l.append((ws, amount))
|
||||
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):
|
||||
@ -100,10 +101,13 @@ class WsClientThread(util.DaemonThread):
|
||||
result = r.get('result')
|
||||
if result is None:
|
||||
continue
|
||||
if method == 'blockchain.address.subscribe':
|
||||
self.network.send([('blockchain.address.get_balance', params)], self.response_queue.put)
|
||||
elif method == 'blockchain.address.get_balance':
|
||||
addr = params[0]
|
||||
if method == 'blockchain.scripthash.subscribe':
|
||||
self.network.send([('blockchain.scripthash.get_balance', params)], self.response_queue.put)
|
||||
elif method == 'blockchain.scripthash.get_balance':
|
||||
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, [])
|
||||
for ws, amount in l:
|
||||
if not ws.closed:
|
||||
|
||||
@ -25,6 +25,12 @@ try:
|
||||
except ImportError:
|
||||
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():
|
||||
def __init__(self, hidDevice):
|
||||
self.dongleObject = btchip(hidDevice)
|
||||
@ -46,8 +52,23 @@ class Ledger_Client():
|
||||
return ""
|
||||
|
||||
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):
|
||||
self.checkDevice()
|
||||
# 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.handler.show_message("Computing master public key")
|
||||
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():
|
||||
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('/')
|
||||
if splitPath[0] == 'm':
|
||||
splitPath = splitPath[1:]
|
||||
@ -68,7 +89,7 @@ class Ledger_Client():
|
||||
if len(splitPath) > 1:
|
||||
prevPath = "/".join(splitPath[0:len(splitPath) - 1])
|
||||
nodeData = self.dongleObject.getWalletPublicKey(prevPath)
|
||||
publicKey = compress_public_key(nodeData['publicKey'])#
|
||||
publicKey = compress_public_key(nodeData['publicKey'])
|
||||
h = hashlib.new('ripemd160')
|
||||
h.update(hashlib.sha256(publicKey).digest())
|
||||
fingerprint = unpack(">I", h.digest()[0:4])[0]
|
||||
@ -119,7 +140,7 @@ class Ledger_Client():
|
||||
|
||||
if not checkFirmware(firmware):
|
||||
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:
|
||||
self.dongleObject.getOperationMode()
|
||||
except BTChipException as e:
|
||||
@ -183,14 +204,14 @@ class Ledger_KeyStore(Hardware_KeyStore):
|
||||
return obj
|
||||
|
||||
def get_derivation(self):
|
||||
return self.derivation
|
||||
return self.derivation
|
||||
|
||||
def get_client(self):
|
||||
return self.plugin.get_client(self).dongleObject
|
||||
|
||||
|
||||
def get_client_electrum(self):
|
||||
return self.plugin.get_client(self)
|
||||
|
||||
|
||||
def give_error(self, message, clear_client = False):
|
||||
print_error(message)
|
||||
if not self.signing:
|
||||
@ -285,12 +306,12 @@ class Ledger_KeyStore(Hardware_KeyStore):
|
||||
|
||||
if txin['type'] in ['p2wpkh-p2sh', 'p2wsh-p2sh']:
|
||||
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
|
||||
|
||||
if txin['type'] in ['p2wpkh', 'p2wsh']:
|
||||
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
|
||||
|
||||
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..."))
|
||||
try:
|
||||
# Get trusted inputs from the original transactions
|
||||
for utxo in inputs:
|
||||
for utxo in inputs:
|
||||
sequence = int_to_hex(utxo[5], 4)
|
||||
if segwitTransaction:
|
||||
txtmp = bitcoinTransaction(bfh(utxo[0]))
|
||||
txtmp = bitcoinTransaction(bfh(utxo[0]))
|
||||
tmp = bfh(utxo[3])[::-1]
|
||||
tmp += bfh(int_to_hex(utxo[1], 4))
|
||||
tmp += txtmp.outputs[utxo[1]].amount
|
||||
@ -434,7 +455,7 @@ class LedgerPlugin(HW_PluginBase):
|
||||
libraries_available = BTCHIP
|
||||
keystore_class = Ledger_KeyStore
|
||||
client = None
|
||||
DEVICE_IDS = [
|
||||
DEVICE_IDS = [
|
||||
(0x2581, 0x1807), # HW.1 legacy btchip
|
||||
(0x2581, 0x2b7c), # HW.1 transitional production
|
||||
(0x2581, 0x3b7c), # HW.1 ledger production
|
||||
@ -459,12 +480,12 @@ class LedgerPlugin(HW_PluginBase):
|
||||
def get_btchip_device(self, device):
|
||||
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):
|
||||
ledger = True
|
||||
ledger = True
|
||||
dev = hid.device()
|
||||
dev.open_path(device.path)
|
||||
dev.set_nonblocking(True)
|
||||
return HIDDongleHIDAPI(dev, ledger, BTCHIP_DEBUG)
|
||||
|
||||
|
||||
def create_client(self, device, handler):
|
||||
self.handler = handler
|
||||
|
||||
@ -473,7 +494,7 @@ class LedgerPlugin(HW_PluginBase):
|
||||
client = Ledger_Client(client)
|
||||
return client
|
||||
|
||||
def setup_device(self, device_info, wizard):
|
||||
def setup_device(self, device_info, wizard):
|
||||
devmgr = self.device_manager()
|
||||
device_id = device_info.device.id_
|
||||
client = devmgr.client_by_id(device_id)
|
||||
@ -494,10 +515,10 @@ class LedgerPlugin(HW_PluginBase):
|
||||
devmgr = self.device_manager()
|
||||
handler = keystore.handler
|
||||
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
|
||||
#if client:
|
||||
# client.used()
|
||||
if client is not None:
|
||||
client.checkDevice()
|
||||
client.checkDevice()
|
||||
return client
|
||||
|
||||
Loading…
Reference in New Issue
Block a user