commit
cd1a520afc
|
@ -1,6 +1,5 @@
|
||||||
from electrum.util import print_error
|
from electrum.util import print_error
|
||||||
|
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
import requests
|
import requests
|
||||||
import threading
|
import threading
|
||||||
|
@ -28,17 +27,17 @@ from electrum_gui.qt.util import ThreadedButton, Buttons, CancelButton, OkButton
|
||||||
|
|
||||||
class Plugin(BasePlugin):
|
class Plugin(BasePlugin):
|
||||||
|
|
||||||
target_host = 'labelectrum.herokuapp.com'
|
target_host = 'sync.bysh.me:8080'
|
||||||
encode_password = None
|
encode_password = None
|
||||||
|
|
||||||
def fullname(self):
|
def fullname(self):
|
||||||
return _('Label Sync')
|
return _('LabelSync')
|
||||||
|
|
||||||
def description(self):
|
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):
|
def version(self):
|
||||||
return "0.2.1"
|
return "0.0.1"
|
||||||
|
|
||||||
def encode(self, message):
|
def encode(self, message):
|
||||||
encrypted = electrum.bitcoin.aes_encrypt_with_iv(self.encode_password, self.iv, message.encode('utf8'))
|
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')
|
decoded_message = electrum.bitcoin.aes_decrypt_with_iv(self.encode_password, self.iv, base64.b64decode(message)).decode('utf8')
|
||||||
return decoded_message
|
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
|
@hook
|
||||||
def init_qt(self, gui):
|
def init_qt(self, gui):
|
||||||
self.window = gui.main_window
|
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)
|
self.window.connect(self.window, SIGNAL('labels:pulled'), self.on_pulled)
|
||||||
|
|
||||||
@hook
|
@hook
|
||||||
def load_wallet(self, wallet):
|
def load_wallet(self, wallet):
|
||||||
self.wallet = 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()))
|
mpk = ''.join(sorted(self.wallet.get_master_public_keys().values()))
|
||||||
self.encode_password = hashlib.sha1(mpk).digest().encode('hex')[:32]
|
self.encode_password = hashlib.sha1(mpk).digest().encode('hex')[:32]
|
||||||
self.iv = hashlib.sha256(self.encode_password).digest()[:16]
|
self.iv = hashlib.sha256(self.encode_password).digest()[:16]
|
||||||
self.wallet_id = hashlib.sha256(mpk).digest().encode('hex')
|
self.wallet_id = hashlib.sha256(mpk).digest().encode('hex')
|
||||||
|
|
||||||
addresses = []
|
addresses = []
|
||||||
for account in self.wallet.accounts.values():
|
for account in self.wallet.accounts.values():
|
||||||
for address in account.get_addresses(0):
|
for address in account.get_addresses(0):
|
||||||
addresses.append(address)
|
addresses.append(address)
|
||||||
|
|
||||||
self.addresses = addresses
|
self.addresses = addresses
|
||||||
|
|
||||||
if self.auth_token():
|
# If there is an auth token we can try to actually start syncing
|
||||||
# If there is an auth token we can try to actually start syncing
|
def do_pull_thread():
|
||||||
def do_pull_thread():
|
try:
|
||||||
try:
|
self.pull_thread()
|
||||||
self.pull_thread()
|
except Exception as e:
|
||||||
except:
|
print_error("could not retrieve labels:", e)
|
||||||
print_error("could not retrieve labels")
|
t = threading.Thread(target=do_pull_thread)
|
||||||
t = threading.Thread(target=do_pull_thread)
|
t.setDaemon(True)
|
||||||
t.setDaemon(True)
|
t.start()
|
||||||
t.start()
|
|
||||||
|
|
||||||
def auth_token(self):
|
|
||||||
return self.config.get("plugin_label_api_key")
|
|
||||||
|
|
||||||
def is_available(self):
|
def is_available(self):
|
||||||
return True
|
return True
|
||||||
|
@ -104,43 +102,21 @@ class Plugin(BasePlugin):
|
||||||
return
|
return
|
||||||
if not changed:
|
if not changed:
|
||||||
return
|
return
|
||||||
bundle = {"label": {"external_id": self.encode(item), "text": self.encode(label)}}
|
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", False, bundle])
|
t = threading.Thread(target=self.do_request, args=["POST", "/label", False, bundle])
|
||||||
t.start()
|
t.start()
|
||||||
|
self.set_nonce(self.wallet.storage.get("wallet_nonce") + 1)
|
||||||
|
|
||||||
def settings_widget(self, window):
|
def settings_widget(self, window):
|
||||||
return EnterButton(_('Settings'), self.settings_dialog)
|
return EnterButton(_('Settings'), self.settings_dialog)
|
||||||
|
|
||||||
def settings_dialog(self):
|
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()
|
d = QDialog()
|
||||||
vbox = QVBoxLayout(d)
|
vbox = QVBoxLayout(d)
|
||||||
layout = QGridLayout()
|
layout = QGridLayout()
|
||||||
vbox.addLayout(layout)
|
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(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)
|
self.upload = ThreadedButton("Force upload", self.push_thread, self.done_processing)
|
||||||
layout.addWidget(self.upload, 2, 1)
|
layout.addWidget(self.upload, 2, 1)
|
||||||
|
@ -151,8 +127,6 @@ class Plugin(BasePlugin):
|
||||||
self.accept = OkButton(d, _("Done"))
|
self.accept = OkButton(d, _("Done"))
|
||||||
vbox.addLayout(Buttons(CancelButton(d), self.accept))
|
vbox.addLayout(Buttons(CancelButton(d), self.accept))
|
||||||
|
|
||||||
check_for_api_key(self.auth_token())
|
|
||||||
|
|
||||||
if d.exec_():
|
if d.exec_():
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
@ -166,8 +140,8 @@ class Plugin(BasePlugin):
|
||||||
def done_processing(self):
|
def done_processing(self):
|
||||||
QMessageBox.information(None, _("Labels synchronised"), _("Your labels have been synchronised."))
|
QMessageBox.information(None, _("Labels synchronised"), _("Your labels have been synchronised."))
|
||||||
|
|
||||||
def do_request(self, method, is_batch=False, data=None):
|
def do_request(self, method, url = "/labels", 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())
|
url = 'http://' + self.target_host + url
|
||||||
kwargs = {'headers': {}}
|
kwargs = {'headers': {}}
|
||||||
if method == 'GET' and data:
|
if method == 'GET' and data:
|
||||||
kwargs['params'] = data
|
kwargs['params'] = data
|
||||||
|
@ -183,7 +157,7 @@ class Plugin(BasePlugin):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def push_thread(self):
|
def push_thread(self):
|
||||||
bundle = {"labels": {}}
|
bundle = {"labels": [], "walletId": self.wallet_id, "walletNonce": self.wallet_nonce}
|
||||||
for key, value in self.wallet.labels.iteritems():
|
for key, value in self.wallet.labels.iteritems():
|
||||||
try:
|
try:
|
||||||
encoded_key = self.encode(key)
|
encoded_key = self.encode(key)
|
||||||
|
@ -191,32 +165,40 @@ class Plugin(BasePlugin):
|
||||||
except:
|
except:
|
||||||
print_error('cannot encode', repr(key), repr(value))
|
print_error('cannot encode', repr(key), repr(value))
|
||||||
continue
|
continue
|
||||||
bundle["labels"][encoded_key] = encoded_value
|
bundle["labels"].append({'encryptedLabel': encoded_value, 'externalId': encoded_key})
|
||||||
self.do_request("POST", True, bundle)
|
self.do_request("POST", "/labels", True, bundle)
|
||||||
|
|
||||||
def pull_thread(self, force = False):
|
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 = {}
|
result = {}
|
||||||
for label in response:
|
if not response["labels"] is None:
|
||||||
try:
|
for label in response["labels"]:
|
||||||
key = self.decode(label["external_id"])
|
try:
|
||||||
value = self.decode(label["text"])
|
key = self.decode(label["externalId"])
|
||||||
except:
|
value = self.decode(label["encryptedLabel"])
|
||||||
continue
|
except:
|
||||||
try:
|
continue
|
||||||
json.dumps(key)
|
try:
|
||||||
json.dumps(value)
|
json.dumps(key)
|
||||||
except:
|
json.dumps(value)
|
||||||
print_error('error: no json', key)
|
except:
|
||||||
continue
|
print_error('error: no json', key)
|
||||||
result[key] = value
|
continue
|
||||||
|
result[key] = value
|
||||||
|
|
||||||
wallet = self.wallet
|
wallet = self.wallet
|
||||||
if not wallet:
|
if not wallet:
|
||||||
return
|
return
|
||||||
for key, value in result.items():
|
for key, value in result.items():
|
||||||
if force or not wallet.labels.get(key):
|
if force or not wallet.labels.get(key):
|
||||||
wallet.labels[key] = value
|
wallet.labels[key] = value
|
||||||
|
|
||||||
self.window.emit(SIGNAL('labels:pulled'))
|
self.window.emit(SIGNAL('labels:pulled'))
|
||||||
print_error("received %d labels"%len(response))
|
self.set_nonce(response["nonce"] + 1)
|
||||||
|
print_error("received %d labels"%len(response))
|
||||||
|
|
Loading…
Reference in New Issue