From ebb9faeb6e6922c2d0b72065906e50bd6456d5ea Mon Sep 17 00:00:00 2001 From: Maran Date: Thu, 9 Apr 2015 18:05:52 +0200 Subject: [PATCH 1/2] New LabelSync This LabelSync is much faster because it will only request labels that changed since the last sync. It is also using a new back-end and no longer requires any registration. --- plugins/electrum_sync.py | 204 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 plugins/electrum_sync.py diff --git a/plugins/electrum_sync.py b/plugins/electrum_sync.py new file mode 100644 index 00000000..520f9833 --- /dev/null +++ b/plugins/electrum_sync.py @@ -0,0 +1,204 @@ +from electrum.util import print_error + +import socket +import requests +import threading +import hashlib +import json + +try: + import PyQt4 +except Exception: + sys.exit("Error: Could not import PyQt4 on Linux systems, you may try 'sudo apt-get install python-qt4'") + +from PyQt4.QtGui import * +from PyQt4.QtCore import * +import PyQt4.QtCore as QtCore +import PyQt4.QtGui as QtGui +import aes +import base64 + +import electrum +from electrum.plugins import BasePlugin, hook +from electrum.i18n import _ + +from electrum_gui.qt import HelpButton, EnterButton +from electrum_gui.qt.util import ThreadedButton, Buttons, CancelButton, OkButton + +class Plugin(BasePlugin): + + target_host = 'sync.bysh.me:8080' + encode_password = None + + def fullname(self): + return _('Electrum Sync') + + def description(self): + return '%s\n\n%s' % (_("The new and improved LabelSync plugin. This can sync your labels across multiple Electrum installs by using a remote database to save your data. Labels, transactions ids and addresses are encrypted before they are sent to the remote server."), _("The label sync's server software is open-source as well and can be found on github.com/maran/electrum-sync-server")) + + def version(self): + return "0.0.1" + + def encode(self, message): + encrypted = electrum.bitcoin.aes_encrypt_with_iv(self.encode_password, self.iv, message.encode('utf8')) + encoded_message = base64.b64encode(encrypted) + return encoded_message + + def decode(self, message): + decoded_message = electrum.bitcoin.aes_decrypt_with_iv(self.encode_password, self.iv, base64.b64decode(message)).decode('utf8') + return decoded_message + + def set_nonce(self, nonce): + print_error("Set nonce to", nonce) + self.wallet.storage.put("wallet_nonce", nonce, True) + self.wallet_nonce = nonce + + @hook + def init_qt(self, gui): + self.window = gui.main_window + self.window.connect(self.window, SIGNAL('labels:pulled'), self.on_pulled) + + @hook + def load_wallet(self, wallet): + self.wallet = wallet + + self.wallet_nonce = self.wallet.storage.get("wallet_nonce") + print_error("Wallet nonce is", self.wallet_nonce) + if self.wallet_nonce is None: + self.set_nonce(1) + + mpk = ''.join(sorted(self.wallet.get_master_public_keys().values())) + self.encode_password = hashlib.sha1(mpk).digest().encode('hex')[:32] + self.iv = hashlib.sha256(self.encode_password).digest()[:16] + self.wallet_id = hashlib.sha256(mpk).digest().encode('hex') + + addresses = [] + for account in self.wallet.accounts.values(): + for address in account.get_addresses(0): + addresses.append(address) + + self.addresses = addresses + + # If there is an auth token we can try to actually start syncing + def do_pull_thread(): + try: + self.pull_thread() + except Exception as e: + print_error("could not retrieve labels:", e) + t = threading.Thread(target=do_pull_thread) + t.setDaemon(True) + t.start() + + + def is_available(self): + return True + + def requires_settings(self): + return True + + @hook + def set_label(self, item,label, changed): + if self.encode_password is None: + return + if not changed: + return + bundle = {"walletId": self.wallet_id, "walletNonce": self.wallet.storage.get("wallet_nonce"), "externalId": self.encode(item), "encryptedLabel": self.encode(label)} + t = threading.Thread(target=self.do_request, args=["POST", "/label", False, bundle]) + t.start() + self.set_nonce(self.wallet.storage.get("wallet_nonce") + 1) + + def settings_widget(self, window): + return EnterButton(_('Settings'), self.settings_dialog) + + def settings_dialog(self): + d = QDialog() + vbox = QVBoxLayout(d) + layout = QGridLayout() + vbox.addLayout(layout) + + layout.addWidget(QLabel("Label sync options: "),2,0) + + self.upload = ThreadedButton("Force upload", self.push_thread, self.done_processing) + layout.addWidget(self.upload, 2, 1) + + self.download = ThreadedButton("Force download", lambda: self.pull_thread(True), self.done_processing) + layout.addWidget(self.download, 2, 2) + + self.accept = OkButton(d, _("Done")) + vbox.addLayout(Buttons(CancelButton(d), self.accept)) + + if d.exec_(): + return True + else: + return False + + def on_pulled(self): + wallet = self.wallet + wallet.storage.put('labels', wallet.labels, True) + self.window.labelsChanged.emit() + + def done_processing(self): + QMessageBox.information(None, _("Labels synchronised"), _("Your labels have been synchronised.")) + + def do_request(self, method, url = "/labels", is_batch=False, data=None): + url = 'http://' + self.target_host + url + kwargs = {'headers': {}} + if method == 'GET' and data: + kwargs['params'] = data + elif method == 'POST' and data: + kwargs['data'] = json.dumps(data) + kwargs['headers']['Content-Type'] = 'application/json' + response = requests.request(method, url, **kwargs) + if response.status_code != 200: + raise BaseException(response.status_code, response.text) + response = response.json() + if "error" in response: + raise BaseException(response["error"]) + return response + + def push_thread(self): + bundle = {"labels": [], "walletId": self.wallet_id, "walletNonce": self.wallet_nonce} + for key, value in self.wallet.labels.iteritems(): + try: + encoded_key = self.encode(key) + encoded_value = self.encode(value) + except: + print_error('cannot encode', repr(key), repr(value)) + continue + bundle["labels"].append({'encryptedLabel': encoded_value, 'externalId': encoded_key}) + self.do_request("POST", "/labels", True, bundle) + + def pull_thread(self, force = False): + if force: + wallet_nonce = 1 + else: + wallet_nonce = self.wallet_nonce - 1 + + print_error("Asking for labels since nonce", wallet_nonce) + response = self.do_request("GET", ("/labels/since/%d/for/%s" % (wallet_nonce, self.wallet_id) )) + result = {} + if not response["labels"] is None: + for label in response["labels"]: + try: + key = self.decode(label["externalId"]) + value = self.decode(label["encryptedLabel"]) + except: + continue + try: + json.dumps(key) + json.dumps(value) + except: + print_error('error: no json', key) + continue + result[key] = value + + wallet = self.wallet + if not wallet: + return + for key, value in result.items(): + if force or not wallet.labels.get(key): + wallet.labels[key] = value + + self.window.emit(SIGNAL('labels:pulled')) + self.set_nonce(response["nonce"] + 1) + print_error("received %d labels"%len(response)) From 7356d41240f002cf6e92779350b2d9fceedbf15c Mon Sep 17 00:00:00 2001 From: Maran Date: Sat, 11 Apr 2015 09:57:01 +0200 Subject: [PATCH 2/2] Migrate the new function into the old plugin --- plugins/electrum_sync.py | 204 --------------------------------------- plugins/labels.py | 144 ++++++++++++--------------- 2 files changed, 63 insertions(+), 285 deletions(-) delete mode 100644 plugins/electrum_sync.py diff --git a/plugins/electrum_sync.py b/plugins/electrum_sync.py deleted file mode 100644 index 520f9833..00000000 --- a/plugins/electrum_sync.py +++ /dev/null @@ -1,204 +0,0 @@ -from electrum.util import print_error - -import socket -import requests -import threading -import hashlib -import json - -try: - import PyQt4 -except Exception: - sys.exit("Error: Could not import PyQt4 on Linux systems, you may try 'sudo apt-get install python-qt4'") - -from PyQt4.QtGui import * -from PyQt4.QtCore import * -import PyQt4.QtCore as QtCore -import PyQt4.QtGui as QtGui -import aes -import base64 - -import electrum -from electrum.plugins import BasePlugin, hook -from electrum.i18n import _ - -from electrum_gui.qt import HelpButton, EnterButton -from electrum_gui.qt.util import ThreadedButton, Buttons, CancelButton, OkButton - -class Plugin(BasePlugin): - - target_host = 'sync.bysh.me:8080' - encode_password = None - - def fullname(self): - return _('Electrum Sync') - - def description(self): - return '%s\n\n%s' % (_("The new and improved LabelSync plugin. This can sync your labels across multiple Electrum installs by using a remote database to save your data. Labels, transactions ids and addresses are encrypted before they are sent to the remote server."), _("The label sync's server software is open-source as well and can be found on github.com/maran/electrum-sync-server")) - - def version(self): - return "0.0.1" - - def encode(self, message): - encrypted = electrum.bitcoin.aes_encrypt_with_iv(self.encode_password, self.iv, message.encode('utf8')) - encoded_message = base64.b64encode(encrypted) - return encoded_message - - def decode(self, message): - decoded_message = electrum.bitcoin.aes_decrypt_with_iv(self.encode_password, self.iv, base64.b64decode(message)).decode('utf8') - return decoded_message - - def set_nonce(self, nonce): - print_error("Set nonce to", nonce) - self.wallet.storage.put("wallet_nonce", nonce, True) - self.wallet_nonce = nonce - - @hook - def init_qt(self, gui): - self.window = gui.main_window - self.window.connect(self.window, SIGNAL('labels:pulled'), self.on_pulled) - - @hook - def load_wallet(self, wallet): - self.wallet = wallet - - self.wallet_nonce = self.wallet.storage.get("wallet_nonce") - print_error("Wallet nonce is", self.wallet_nonce) - if self.wallet_nonce is None: - self.set_nonce(1) - - mpk = ''.join(sorted(self.wallet.get_master_public_keys().values())) - self.encode_password = hashlib.sha1(mpk).digest().encode('hex')[:32] - self.iv = hashlib.sha256(self.encode_password).digest()[:16] - self.wallet_id = hashlib.sha256(mpk).digest().encode('hex') - - addresses = [] - for account in self.wallet.accounts.values(): - for address in account.get_addresses(0): - addresses.append(address) - - self.addresses = addresses - - # If there is an auth token we can try to actually start syncing - def do_pull_thread(): - try: - self.pull_thread() - except Exception as e: - print_error("could not retrieve labels:", e) - t = threading.Thread(target=do_pull_thread) - t.setDaemon(True) - t.start() - - - def is_available(self): - return True - - def requires_settings(self): - return True - - @hook - def set_label(self, item,label, changed): - if self.encode_password is None: - return - if not changed: - return - bundle = {"walletId": self.wallet_id, "walletNonce": self.wallet.storage.get("wallet_nonce"), "externalId": self.encode(item), "encryptedLabel": self.encode(label)} - t = threading.Thread(target=self.do_request, args=["POST", "/label", False, bundle]) - t.start() - self.set_nonce(self.wallet.storage.get("wallet_nonce") + 1) - - def settings_widget(self, window): - return EnterButton(_('Settings'), self.settings_dialog) - - def settings_dialog(self): - d = QDialog() - vbox = QVBoxLayout(d) - layout = QGridLayout() - vbox.addLayout(layout) - - layout.addWidget(QLabel("Label sync options: "),2,0) - - self.upload = ThreadedButton("Force upload", self.push_thread, self.done_processing) - layout.addWidget(self.upload, 2, 1) - - self.download = ThreadedButton("Force download", lambda: self.pull_thread(True), self.done_processing) - layout.addWidget(self.download, 2, 2) - - self.accept = OkButton(d, _("Done")) - vbox.addLayout(Buttons(CancelButton(d), self.accept)) - - if d.exec_(): - return True - else: - return False - - def on_pulled(self): - wallet = self.wallet - wallet.storage.put('labels', wallet.labels, True) - self.window.labelsChanged.emit() - - def done_processing(self): - QMessageBox.information(None, _("Labels synchronised"), _("Your labels have been synchronised.")) - - def do_request(self, method, url = "/labels", is_batch=False, data=None): - url = 'http://' + self.target_host + url - kwargs = {'headers': {}} - if method == 'GET' and data: - kwargs['params'] = data - elif method == 'POST' and data: - kwargs['data'] = json.dumps(data) - kwargs['headers']['Content-Type'] = 'application/json' - response = requests.request(method, url, **kwargs) - if response.status_code != 200: - raise BaseException(response.status_code, response.text) - response = response.json() - if "error" in response: - raise BaseException(response["error"]) - return response - - def push_thread(self): - bundle = {"labels": [], "walletId": self.wallet_id, "walletNonce": self.wallet_nonce} - for key, value in self.wallet.labels.iteritems(): - try: - encoded_key = self.encode(key) - encoded_value = self.encode(value) - except: - print_error('cannot encode', repr(key), repr(value)) - continue - bundle["labels"].append({'encryptedLabel': encoded_value, 'externalId': encoded_key}) - self.do_request("POST", "/labels", True, bundle) - - def pull_thread(self, force = False): - if force: - wallet_nonce = 1 - else: - wallet_nonce = self.wallet_nonce - 1 - - print_error("Asking for labels since nonce", wallet_nonce) - response = self.do_request("GET", ("/labels/since/%d/for/%s" % (wallet_nonce, self.wallet_id) )) - result = {} - if not response["labels"] is None: - for label in response["labels"]: - try: - key = self.decode(label["externalId"]) - value = self.decode(label["encryptedLabel"]) - except: - continue - try: - json.dumps(key) - json.dumps(value) - except: - print_error('error: no json', key) - continue - result[key] = value - - wallet = self.wallet - if not wallet: - return - for key, value in result.items(): - if force or not wallet.labels.get(key): - wallet.labels[key] = value - - self.window.emit(SIGNAL('labels:pulled')) - self.set_nonce(response["nonce"] + 1) - print_error("received %d labels"%len(response)) diff --git a/plugins/labels.py b/plugins/labels.py index 181b2b70..c533adcb 100644 --- a/plugins/labels.py +++ b/plugins/labels.py @@ -1,6 +1,5 @@ from electrum.util import print_error - import socket import requests import threading @@ -28,17 +27,17 @@ from electrum_gui.qt.util import ThreadedButton, Buttons, CancelButton, OkButton class Plugin(BasePlugin): - target_host = 'labelectrum.herokuapp.com' + target_host = 'sync.bysh.me:8080' encode_password = None def fullname(self): - return _('Label Sync') + return _('LabelSync') def description(self): - return '%s\n\n%s%s%s' % (_("This plugin can sync your labels across multiple Electrum installs by using a remote database to save your data. Labels, transactions ids and addresses are encrypted before they are sent to the remote server. This code might increase the load of your wallet with a few microseconds as it will sync labels on each startup."), _("To get started visit"), " http://labelectrum.herokuapp.com/ ", _(" to sign up for an account.")) + return '%s\n\n%s' % (_("The new and improved LabelSync plugin. This can sync your labels across multiple Electrum installs by using a remote database to save your data. Labels, transactions ids and addresses are encrypted before they are sent to the remote server."), _("The label sync's server software is open-source as well and can be found on github.com/maran/electrum-sync-server")) def version(self): - return "0.2.1" + return "0.0.1" def encode(self, message): encrypted = electrum.bitcoin.aes_encrypt_with_iv(self.encode_password, self.iv, message.encode('utf8')) @@ -49,48 +48,47 @@ class Plugin(BasePlugin): decoded_message = electrum.bitcoin.aes_decrypt_with_iv(self.encode_password, self.iv, base64.b64decode(message)).decode('utf8') return decoded_message + def set_nonce(self, nonce): + print_error("Set nonce to", nonce) + self.wallet.storage.put("wallet_nonce", nonce, True) + self.wallet_nonce = nonce + @hook def init_qt(self, gui): self.window = gui.main_window - if not self.auth_token(): # First run, throw plugin settings in your face - self.load_wallet(self.window.wallet) - if self.settings_dialog(): - self.set_enabled(True) - return True - else: - self.set_enabled(False) - return False - self.window.connect(self.window, SIGNAL('labels:pulled'), self.on_pulled) @hook def load_wallet(self, wallet): self.wallet = wallet + + self.wallet_nonce = self.wallet.storage.get("wallet_nonce") + print_error("Wallet nonce is", self.wallet_nonce) + if self.wallet_nonce is None: + self.set_nonce(1) + mpk = ''.join(sorted(self.wallet.get_master_public_keys().values())) self.encode_password = hashlib.sha1(mpk).digest().encode('hex')[:32] self.iv = hashlib.sha256(self.encode_password).digest()[:16] self.wallet_id = hashlib.sha256(mpk).digest().encode('hex') - addresses = [] + addresses = [] for account in self.wallet.accounts.values(): for address in account.get_addresses(0): addresses.append(address) self.addresses = addresses - if self.auth_token(): - # If there is an auth token we can try to actually start syncing - def do_pull_thread(): - try: - self.pull_thread() - except: - print_error("could not retrieve labels") - t = threading.Thread(target=do_pull_thread) - t.setDaemon(True) - t.start() + # If there is an auth token we can try to actually start syncing + def do_pull_thread(): + try: + self.pull_thread() + except Exception as e: + print_error("could not retrieve labels:", e) + t = threading.Thread(target=do_pull_thread) + t.setDaemon(True) + t.start() - def auth_token(self): - return self.config.get("plugin_label_api_key") def is_available(self): return True @@ -104,43 +102,21 @@ class Plugin(BasePlugin): return if not changed: return - bundle = {"label": {"external_id": self.encode(item), "text": self.encode(label)}} - t = threading.Thread(target=self.do_request, args=["POST", False, bundle]) + bundle = {"walletId": self.wallet_id, "walletNonce": self.wallet.storage.get("wallet_nonce"), "externalId": self.encode(item), "encryptedLabel": self.encode(label)} + t = threading.Thread(target=self.do_request, args=["POST", "/label", False, bundle]) t.start() + self.set_nonce(self.wallet.storage.get("wallet_nonce") + 1) def settings_widget(self, window): return EnterButton(_('Settings'), self.settings_dialog) def settings_dialog(self): - def check_for_api_key(api_key): - if api_key and len(api_key) > 12: - self.config.set_key("plugin_label_api_key", str(self.auth_token_edit.text())) - self.upload.setEnabled(True) - self.download.setEnabled(True) - self.accept.setEnabled(True) - else: - self.upload.setEnabled(False) - self.download.setEnabled(False) - self.accept.setEnabled(False) - d = QDialog() vbox = QVBoxLayout(d) layout = QGridLayout() vbox.addLayout(layout) - layout.addWidget(QLabel("API Key: "), 0, 0) - - self.auth_token_edit = QLineEdit(self.auth_token()) - self.auth_token_edit.textChanged.connect(check_for_api_key) - layout.addWidget(QLabel("Label sync options: "),2,0) - layout.addWidget(self.auth_token_edit, 0,1,1,2) - - decrypt_key_text = QLineEdit(self.encode_password) - decrypt_key_text.setReadOnly(True) - layout.addWidget(decrypt_key_text, 1,1) - layout.addWidget(QLabel("Decryption key: "),1,0) - layout.addWidget(HelpButton("This key can be used on the LabElectrum website to decrypt your data in case you want to review it online."),1,2) self.upload = ThreadedButton("Force upload", self.push_thread, self.done_processing) layout.addWidget(self.upload, 2, 1) @@ -151,8 +127,6 @@ class Plugin(BasePlugin): self.accept = OkButton(d, _("Done")) vbox.addLayout(Buttons(CancelButton(d), self.accept)) - check_for_api_key(self.auth_token()) - if d.exec_(): return True else: @@ -166,8 +140,8 @@ class Plugin(BasePlugin): def done_processing(self): QMessageBox.information(None, _("Labels synchronised"), _("Your labels have been synchronised.")) - def do_request(self, method, is_batch=False, data=None): - url = 'https://' + self.target_host + "/api/wallets/%s/%s?auth_token=%s" % (self.wallet_id, 'labels/batch.json' if is_batch else 'labels.json', self.auth_token()) + def do_request(self, method, url = "/labels", is_batch=False, data=None): + url = 'http://' + self.target_host + url kwargs = {'headers': {}} if method == 'GET' and data: kwargs['params'] = data @@ -183,7 +157,7 @@ class Plugin(BasePlugin): return response def push_thread(self): - bundle = {"labels": {}} + bundle = {"labels": [], "walletId": self.wallet_id, "walletNonce": self.wallet_nonce} for key, value in self.wallet.labels.iteritems(): try: encoded_key = self.encode(key) @@ -191,32 +165,40 @@ class Plugin(BasePlugin): except: print_error('cannot encode', repr(key), repr(value)) continue - bundle["labels"][encoded_key] = encoded_value - self.do_request("POST", True, bundle) + bundle["labels"].append({'encryptedLabel': encoded_value, 'externalId': encoded_key}) + self.do_request("POST", "/labels", True, bundle) def pull_thread(self, force = False): - response = self.do_request("GET") + if force: + wallet_nonce = 1 + else: + wallet_nonce = self.wallet_nonce - 1 + + print_error("Asking for labels since nonce", wallet_nonce) + response = self.do_request("GET", ("/labels/since/%d/for/%s" % (wallet_nonce, self.wallet_id) )) result = {} - for label in response: - try: - key = self.decode(label["external_id"]) - value = self.decode(label["text"]) - except: - continue - try: - json.dumps(key) - json.dumps(value) - except: - print_error('error: no json', key) - continue - result[key] = value + if not response["labels"] is None: + for label in response["labels"]: + try: + key = self.decode(label["externalId"]) + value = self.decode(label["encryptedLabel"]) + except: + continue + try: + json.dumps(key) + json.dumps(value) + except: + print_error('error: no json', key) + continue + result[key] = value - wallet = self.wallet - if not wallet: - return - for key, value in result.items(): - if force or not wallet.labels.get(key): - wallet.labels[key] = value + wallet = self.wallet + if not wallet: + return + for key, value in result.items(): + if force or not wallet.labels.get(key): + wallet.labels[key] = value - self.window.emit(SIGNAL('labels:pulled')) - print_error("received %d labels"%len(response)) + self.window.emit(SIGNAL('labels:pulled')) + self.set_nonce(response["nonce"] + 1) + print_error("received %d labels"%len(response))