diff --git a/gui/qt/contact_list.py b/gui/qt/contact_list.py index a17944593..81b6ca869 100644 --- a/gui/qt/contact_list.py +++ b/gui/qt/contact_list.py @@ -23,16 +23,17 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import webbrowser +import os from electrum.i18n import _ from electrum.bitcoin import is_address -from electrum.util import block_explorer_URL, FileImportFailed +from electrum.util import block_explorer_URL from electrum.plugins import run_hook from PyQt5.QtGui import * from PyQt5.QtCore import * from PyQt5.QtWidgets import ( QAbstractItemView, QFileDialog, QMenu, QTreeWidgetItem) -from .util import MyTreeWidget +from .util import MyTreeWidget, import_meta_gui, export_meta_gui class ContactList(MyTreeWidget): @@ -53,15 +54,10 @@ class ContactList(MyTreeWidget): self.parent.set_contact(item.text(0), item.text(1)) def import_contacts(self): - wallet_folder = self.parent.get_wallet_folder() - filename, __ = QFileDialog.getOpenFileName(self.parent, "Select your wallet file", wallet_folder) - if not filename: - return - try: - self.parent.contacts.import_file(filename) - except FileImportFailed as e: - self.parent.show_message(str(e)) - self.on_update() + import_meta_gui(self.parent, _('contacts'), self.parent.contacts.import_file, self.on_update) + + def export_contacts(self): + export_meta_gui(self.parent, _('contacts'), self.parent.contacts.export_file) def create_menu(self, position): menu = QMenu() @@ -69,6 +65,7 @@ class ContactList(MyTreeWidget): if not selected: menu.addAction(_("New contact"), lambda: self.parent.new_contact_dialog()) menu.addAction(_("Import file"), lambda: self.import_contacts()) + menu.addAction(_("Export file"), lambda: self.export_contacts()) else: names = [item.text(0) for item in selected] keys = [item.text(1) for item in selected] diff --git a/gui/qt/invoice_list.py b/gui/qt/invoice_list.py index a4a8374f7..d36c4866b 100644 --- a/gui/qt/invoice_list.py +++ b/gui/qt/invoice_list.py @@ -25,7 +25,7 @@ from .util import * from electrum.i18n import _ -from electrum.util import format_time, FileImportFailed +from electrum.util import format_time class InvoiceList(MyTreeWidget): @@ -57,15 +57,10 @@ class InvoiceList(MyTreeWidget): self.parent.invoices_label.setVisible(len(inv_list)) def import_invoices(self): - wallet_folder = self.parent.get_wallet_folder() - filename, __ = QFileDialog.getOpenFileName(self.parent, "Select your wallet file", wallet_folder) - if not filename: - return - try: - self.parent.invoices.import_file(filename) - except FileImportFailed as e: - self.parent.show_message(str(e)) - self.on_update() + import_meta_gui(self.parent, _('invoices'), self.parent.invoices.import_file, self.on_update) + + def export_invoices(self): + export_meta_gui(self.parent, _('invoices'), self.parent.invoices.export_file) def create_menu(self, position): menu = QMenu() diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 6b4a1f9bc..89c0ce1d7 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -61,7 +61,7 @@ from .fee_slider import FeeSlider from .util import * -from electrum.util import profiler +from electrum.util import profiler, export_meta, import_meta class StatusBarButton(QPushButton): def __init__(self, icon, tooltip, func): @@ -484,8 +484,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): contacts_menu = wallet_menu.addMenu(_("Contacts")) contacts_menu.addAction(_("&New"), self.new_contact_dialog) contacts_menu.addAction(_("Import"), lambda: self.contact_list.import_contacts()) + contacts_menu.addAction(_("Export"), lambda: self.contact_list.export_contacts()) invoices_menu = wallet_menu.addMenu(_("Invoices")) invoices_menu.addAction(_("Import"), lambda: self.invoice_list.import_invoices()) + invoices_menu.addAction(_("Export"), lambda: self.invoice_list.export_invoices()) wallet_menu.addSeparator() wallet_menu.addAction(_("Find"), self.toggle_search).setShortcut(QKeySequence("Ctrl+F")) @@ -2417,29 +2419,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): f.write(json.dumps(pklist, indent = 4)) def do_import_labels(self): - labelsFile = self.getOpenFileName(_("Open labels file"), "*.json") - if not labelsFile: return - try: - with open(labelsFile, 'r') as f: - data = f.read() - for key, value in json.loads(data).items(): - self.wallet.set_label(key, value) - self.show_message(_("Your labels were imported from") + " '%s'" % str(labelsFile)) - except (IOError, os.error) as reason: - self.show_critical(_("Electrum was unable to import your labels.") + "\n" + str(reason)) - self.address_list.update() - self.history_list.update() + def import_labels(path): + #TODO: Import labels validation + def import_labels_validate(data): + return data + def import_labels_assign(data): + for key, value in data.items(): + self.wallet.set_label(key, value) + import_meta(path, import_labels_validate, import_labels_assign) + def on_import(): + self.address_list.update() + self.history_list.update() + import_meta_gui(self, _('labels'), import_labels, on_import) def do_export_labels(self): - labels = self.wallet.labels - try: - fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.json', "*.json") - if fileName: - with open(fileName, 'w+') as f: - json.dump(labels, f, indent=4, sort_keys=True) - self.show_message(_("Your labels were exported to") + " '%s'" % str(fileName)) - except (IOError, os.error) as reason: - self.show_critical(_("Electrum was unable to export your labels.") + "\n" + str(reason)) + def export_labels(filename): + export_meta(self.wallet.labels, filename) + export_meta_gui(self, _('labels'), export_labels) def sweep_key_dialog(self): d = WindowModalDialog(self, title=_('Sweep private keys')) diff --git a/gui/qt/util.py b/gui/qt/util.py index 7f0cb238f..60a594bb0 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -11,6 +11,7 @@ from PyQt5.QtGui import * from PyQt5.QtCore import * from PyQt5.QtWidgets import * +from electrum.util import FileImportFailed, FileExportFailed if platform.system() == 'Windows': MONOSPACE_FONT = 'Lucida Console' elif platform.system() == 'Darwin': @@ -674,6 +675,28 @@ class AcceptFileDragDrop: def onFileAdded(self, fn): raise NotImplementedError() +def import_meta_gui(electrum_window, title, importer, on_success): + filename = electrum_window.getOpenFileName(_("Open {} file").format(title) , "*.json") + if not filename: + return + try: + importer(filename) + except FileImportFailed as e: + electrum_window.show_critical(str(e)) + else: + electrum_window.show_message(_("Your {} were successfully imported" ).format(title)) + on_success() + +def export_meta_gui(electrum_window, title, exporter): + filename = electrum_window.getSaveFileName(_("Select file to save your {}").format(title), 'electrum_{}.json'.format(title), "*.json") + if not filename: + return + try: + exporter(filename) + except FileExportFailed as e: + electrum_window.show_critical(str(e)) + else: + electrum_window.show_message(_("Your {0} were exported to '{1}'").format(title,str(filename))) if __name__ == "__main__": app = QApplication([]) diff --git a/lib/contacts.py b/lib/contacts.py index 5157adc41..df10e0863 100644 --- a/lib/contacts.py +++ b/lib/contacts.py @@ -25,10 +25,11 @@ import dns import json import traceback import sys +import os from . import bitcoin from . import dnssec -from .util import FileImportFailed, FileImportFailedEncrypted +from .util import export_meta, import_meta class Contacts(dict): @@ -51,18 +52,15 @@ class Contacts(dict): self.storage.put('contacts', dict(self)) def import_file(self, path): - try: - with open(path, 'r') as f: - d = self._validate(json.loads(f.read())) - except json.decoder.JSONDecodeError: - traceback.print_exc(file=sys.stderr) - raise FileImportFailedEncrypted() - except BaseException: - traceback.print_exc(file=sys.stdout) - raise FileImportFailed() - self.update(d) + import_meta(path, self.validate, self.load_meta) + + def load_meta(self, data): + self.update(data) self.save() + def export_file(self, fileName): + export_meta(self, fileName) + def __setitem__(self, key, value): dict.__setitem__(self, key, value) self.save() @@ -119,7 +117,7 @@ class Contacts(dict): except AttributeError: return None - def _validate(self, data): + def validate(self, data): for k,v in list(data.items()): if k == 'contacts': return self._validate(v) diff --git a/lib/paymentrequest.py b/lib/paymentrequest.py index c1e25441a..878c541e7 100644 --- a/lib/paymentrequest.py +++ b/lib/paymentrequest.py @@ -40,7 +40,7 @@ except ImportError: from . import bitcoin from . import util from .util import print_error, bh2u, bfh -from .util import FileImportFailed, FileImportFailedEncrypted +from .util import export_meta, import_meta from . import transaction from . import x509 from . import rsakey @@ -468,27 +468,31 @@ class InvoiceStore(object): continue def import_file(self, path): - try: - with open(path, 'r') as f: - d = json.loads(f.read()) - self.load(d) - except json.decoder.JSONDecodeError: - traceback.print_exc(file=sys.stderr) - raise FileImportFailedEncrypted() - except BaseException: - traceback.print_exc(file=sys.stdout) - raise FileImportFailed() + import_meta(path, self.validate, self.on_import) + + #TODO: Invoice import validation + def validate(self, data): + return data + + def on_import(self, data): + self.load(data) self.save() - def save(self): - l = {} + def export_file(self, fileName): + export_meta(self.before_save(), fileName) + + def before_save(self): + l= {} for k, pr in self.invoices.items(): l[k] = { 'hex': bh2u(pr.raw), 'requestor': pr.requestor, 'txid': pr.tx } - self.storage.put('invoices', l) + return l + + def save(self): + self.storage.put('invoices', self.before_save()) def get_status(self, key): pr = self.get(key) diff --git a/lib/util.py b/lib/util.py index 9dc95b912..ee66ff3f3 100644 --- a/lib/util.py +++ b/lib/util.py @@ -60,16 +60,18 @@ class InvalidPassword(Exception): class FileImportFailed(Exception): + def __init__(self, message=''): + self.message = str(message) + def __str__(self): - return _("Failed to import file.") + return _("Failed to import from file.") + "\n" + self.message +class FileExportFailed(Exception): + def __init__(self, reason=''): + self.message = str(reason) -class FileImportFailedEncrypted(FileImportFailed): def __str__(self): - return (_('Failed to import file.') + ' ' + - _('Perhaps it is encrypted...') + '\n' + - _('Importing encrypted files is not supported.')) - + return( _("Failed to export to file.") + "\n" + self.message ) # Throw this exception to unwind the stack like when an error occurs. # However unlike other exceptions the user won't be informed. @@ -785,3 +787,24 @@ def setup_thread_excepthook(): def versiontuple(v): return tuple(map(int, (v.split(".")))) + +def import_meta(path, validater, load_meta): + try: + with open(path, 'r') as f: + d = validater(json.loads(f.read())) + load_meta(d) + #backwards compatibility for JSONDecodeError + except ValueError: + traceback.print_exc(file=sys.stderr) + raise FileImportFailed(_("Invalid JSON code.")) + except BaseException as e: + traceback.print_exc(file=sys.stdout) + raise FileImportFailed(e) + +def export_meta(meta, fileName): + try: + with open(fileName, 'w+') as f: + json.dump(meta, f, indent=4, sort_keys=True) + except (IOError, os.error) as reason: + traceback.print_exc(file=sys.stderr) + raise FileExportFailed(str(reason))